Repository: yt-dlp/yt-dlp
Pull request #209 (2021-04-01 21:13:50)

[YouTube] Add support for premium availability state in info json
## Please follow the guide below

- You will be asked some questions, please read them **carefully** and answer honestly
- Put an `x` into all the boxes [ ] relevant to your *pull request* (like that [x])
- Use *Preview* tab to see how your *pull request* will actually look like

---

### Before submitting a *pull request* make sure you have:
- [X] At least skimmed through [adding new extractor tutorial](https://github.com/ytdl-org/youtube-dl#adding-support-for-a-new-site) and [youtube-dl coding conventions](https://github.com/ytdl-org/youtube-dl#youtube-dl-coding-conventions) sections
- [X] [Searched](https://github.com/yt-dlp/yt-dlp/search?q=is%3Apr&type=Issues) the bugtracker for similar pull requests
- [X] Checked the code with [flake8](https://pypi.python.org/pypi/flake8)

### In order to be accepted and merged into youtube-dl each piece of code must be in public domain or released under [Unlicense](http://unlicense.org/). Check one of the following options:
- [X] I am the original author of this code and I am willing to release it under [Unlicense](http://unlicense.org/)
- [ ] I am not the original author of this code but it is in public domain or released under [Unlicense](http://unlicense.org/) (provide reliable evidence)

### What is the purpose of your *pull request*?
- [ ] Bug fix
- [X] Improvement
- [ ] New extractor
- [ ] New feature

---

### Description of your *pull request* and other information

Explanation of your *pull request* in arbitrary form goes here. Please make sure the description explains the purpose and effect of your *pull request* and is worded well enough to be understood. Provide as much context and examples as possible.

As title. Premium label appears as one of the video badges. 

Note: needs testing on an account that has premium.



Patch:
diff --git a/README.md b/README.md
index 42609fa2a..ca3d19fde 100644
--- a/README.md
+++ b/README.md
@@ -46,6 +46,8 @@ # YT-DLP
     * [Filtering Formats](#filtering-formats)
     * [Sorting Formats](#sorting-formats)
     * [Format Selection examples](#format-selection-examples)
+* [MODIFYING METADATA](#modifying-metadata)
+    * [Modifying metadata examples](#modifying-metadata-examples)
 * [PLUGINS](#plugins)
 * [DEPRECATED OPTIONS](#deprecated-options)
 * [MORE](#more)
@@ -669,26 +671,9 @@ ## Post-Processing Options:
     --no-embed-thumbnail             Do not embed thumbnail (default)
     --add-metadata                   Write metadata to the video file
     --no-add-metadata                Do not write metadata (default)
-    --parse-metadata FIELD:FORMAT    Parse additional metadata like title/artist
-                                     from other fields. Give a template or field
-                                     name to extract data from and the format to
-                                     interpret it as, separated by a ":". Either
-                                     regular expression with named capture
-                                     groups or a similar syntax to the output
-                                     template can be used for the FORMAT.
-                                     Similarly, the syntax for output template
-                                     can be used for FIELD to parse the data
-                                     from multiple fields. The parsed parameters
-                                     replace any existing values and can be used
-                                     in output templates. This option can be
-                                     used multiple times. Example: --parse-
-                                     metadata "title:%(artist)s - %(title)s"
-                                     matches a title like "Coldplay - Paradise".
-                                     Example: --parse-metadata "%(series)s
-                                     %(episode_number)s:%(title)s" sets the
-                                     title using series and episode number.
-                                     Example (regex): --parse-metadata
-                                     "description:Artist - (?P<artist>.+?)"
+    --parse-metadata FROM:TO         Parse additional metadata like title/artist
+                                     from other fields; see "MODIFYING METADATA"
+                                     for details
     --xattrs                         Write metadata to the video file's xattrs
                                      (using dublin core and xdg standards)
     --fixup POLICY                   Automatically correct known faults of the
@@ -983,7 +968,7 @@ # FORMAT SELECTION
 By default, yt-dlp tries to download the best available quality if you **don't** pass any options.
 This is generally equivalent to using `-f bestvideo*+bestaudio/best`. However, if multiple audiostreams is enabled (`--audio-multistreams`), the default format changes to `-f bestvideo+bestaudio/best`. Similarly, if ffmpeg is unavailable, or if you use yt-dlp to stream to `stdout` (`-o -`), the default becomes `-f best/bestvideo+bestaudio`.
 
-The general syntax for format selection is `--f FORMAT` (or `--format FORMAT`) where `FORMAT` is a *selector expression*, i.e. an expression that describes format or formats you would like to download.
+The general syntax for format selection is `-f FORMAT` (or `--format FORMAT`) where `FORMAT` is a *selector expression*, i.e. an expression that describes format or formats you would like to download.
 
 **tl;dr:** [navigate me to examples](#format-selection-examples).
 
@@ -1007,7 +992,9 @@ # FORMAT SELECTION
  - `ba*`, `bestaudio*`: Select the best quality format that contains audio. It may also contain video. Equivalent to `best*[acodec!=none]`
  - `wa*`, `worstaudio*`: Select the worst quality format that contains audio. It may also contain video. Equivalent to `worst*[acodec!=none]`
 
-For example, to download the worst quality video-only format you can use `-f worstvideo`. It is however recomended to never actually use `worst` and related options. When your format selector is `worst`, the format which is worst in all respects is selected. Most of the time, what you actually want is the video with the smallest filesize instead. So it is generally better to use `-f best -S +size,+br,+res,+fps` instead of `-f worst`. See [sorting formats](#sorting-formats) for more details.
+For example, to download the worst quality video-only format you can use `-f worstvideo`. It is however recomended not to use `worst` and related options. When your format selector is `worst`, the format which is worst in all respects is selected. Most of the time, what you actually want is the video with the smallest filesize instead. So it is generally better to use `-f best -S +size,+br,+res,+fps` instead of `-f worst`. See [sorting formats](#sorting-formats) for more details.
+
+You can select the n'th best format of a type by using `best<type>.<n>`. For example, `best.2` will select the 2nd best combined format. Similarly, `bv*.3` will select the 3rd best format that contains a video stream.
 
 If you want to download multiple videos and they don't have the same formats available, you can specify the order of preference using slashes. Note that formats on the left hand side are preferred, for example `-f 22/17/18` will download format 22 if it's available, otherwise it will download format 17 if it's available, otherwise it will download format 18 if it's available, otherwise it will complain that no suitable formats are available for download.
 
@@ -1203,6 +1190,33 @@ # prefering better codec and then larger total bitrate for the same resolution
 $ yt-dlp -S '+res:480,codec,br'
 ```
 
+# MODIFYING METADATA
+
+The metadata obtained the the extractors can be modified by using `--parse-metadata FROM:TO`. The general syntax is to give the name of a field or a template (with similar syntax to [output template](#output-template)) to extract data from, and the format to interpret it as, separated by a ":". Either a [python regular expression](https://docs.python.org/3/library/re.html#regular-expression-syntax) with named capture groups or a similar syntax to the [output template](#output-template) (only `%(field)s` formatting is supported) can be used for `TO`. The option can be used multiple times to parse and modify various fields.
+
+Note that any field created by this can be used in the [output template](#output-template) and will also affect the media file's metadata added when using `--add-metadata`.
+
+You can also use this to change only the metadata that is embedded in the media file. To do this, set the value of the corresponding field with a `meta_` prefix. For example, any value you set to `meta_description` field will be added to the `description` field in the file. You can use this to set a different "description" and "synopsis", for example.
+
+## Modifying metadata examples
+
+Note that on Windows you may need to use double quotes instead of single.
+
+```bash
+# Interpret the title as "Artist - Title"
+$ yt-dlp --parse-metadata "title:%(artist)s - %(title)s"
+
+# Regex example
+$ yt-dlp --parse-metadata "description:Artist - (?P<artist>.+)"
+
+# Set title as "Series name S01E05"
+$ yt-dlp --parse-metadata "%(series)s S%(season_number)02dE%(episode_number)02d:%(title)s"
+
+# Set "comment" field in video metadata using description instead of webpage_url
+$ yt-dlp --parse-metadata "description:(?s)(?P<meta_comment>.+)" --add-metadata
+
+```
+
 # PLUGINS
 
 Plugins are loaded from `<root-dir>/ytdlp_plugins/<type>/__init__.py`. Currently only `extractor` plugins are supported. Support for `downloader` and `postprocessor` plugins may be added in the future. See [ytdlp_plugins](ytdlp_plugins) for example.
@@ -1219,7 +1233,7 @@ # DEPRECATED OPTIONS
     -t, --title                      -o "%(title)s-%(id)s.%(ext)s"
     -l, --literal                    -o accepts literal names
     --autonumber-size NUMBER         Use string formatting. Eg: %(autonumber)03d
-    --metadata-from-title FORMAT     --parse-metadata "title:FORMAT"
+    --metadata-from-title FORMAT     --parse-metadata "%(title)s:FORMAT"
     --prefer-avconv                  avconv is no longer officially supported (Alias: --no-prefer-ffmpeg)
     --prefer-ffmpeg                  Default (Alias: --no-prefer-avconv)
     --avconv-location                avconv is no longer officially supported
diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py
index d1b6b2056..a77e1fe5e 100644
--- a/yt_dlp/YoutubeDL.py
+++ b/yt_dlp/YoutubeDL.py
@@ -1576,21 +1576,25 @@ def selector_function(ctx):
 
                 else:
                     format_fallback = False
-                    format_spec_obj = re.match(r'(best|worst|b|w)(video|audio|v|a)?(\*)?$', format_spec)
-                    if format_spec_obj is not None:
-                        format_idx = 0 if format_spec_obj.group(1)[0] == 'w' else -1
-                        format_type = format_spec_obj.group(2)[0] if format_spec_obj.group(2) else False
-                        not_format_type = 'v' if format_type == 'a' else 'a'
-                        format_modified = format_spec_obj.group(3) is not None
+                    mobj = re.match(
+                        r'(?P<bw>best|worst|b|w)(?P<type>video|audio|v|a)?(?P<mod>\*)?(?:\.(?P<n>[1-9]\d*))?$',
+                        format_spec)
+                    if mobj is not None:
+                        format_idx = int_or_none(mobj.group('n'), default=1)
+                        format_idx = format_idx - 1 if mobj.group('bw')[0] == 'w' else -format_idx
+                        format_type = (mobj.group('type') or [None])[0]
+                        not_format_type = {'v': 'a', 'a': 'v'}.get(format_type)
+                        format_modified = mobj.group('mod') is not None
 
                         format_fallback = not format_type and not format_modified  # for b, w
-                        filter_f = ((lambda f: f.get(format_type + 'codec') != 'none')
-                                    if format_type and format_modified  # bv*, ba*, wv*, wa*
-                                    else (lambda f: f.get(not_format_type + 'codec') == 'none')
-                                    if format_type  # bv, ba, wv, wa
-                                    else (lambda f: f.get('vcodec') != 'none' and f.get('acodec') != 'none')
-                                    if not format_modified  # b, w
-                                    else None)  # b*, w*
+                        filter_f = (
+                            (lambda f: f.get('%scodec' % format_type) != 'none')
+                            if format_type and format_modified  # bv*, ba*, wv*, wa*
+                            else (lambda f: f.get('%scodec' % not_format_type) == 'none')
+                            if format_type  # bv, ba, wv, wa
+                            else (lambda f: f.get('vcodec') != 'none' and f.get('acodec') != 'none')
+                            if not format_modified  # b, w
+                            else None)  # b*, w*
                     else:
                         format_idx = -1
                         filter_f = ((lambda f: f.get('ext') == format_spec)
@@ -1602,13 +1606,16 @@ def selector_function(ctx):
                         if not formats:
                             return
                         matches = list(filter(filter_f, formats)) if filter_f is not None else formats
-                        if matches:
+                        n = len(matches)
+                        if -n <= format_idx < n:
                             yield matches[format_idx]
-                        elif format_fallback == 'force' or (format_fallback and ctx['incomplete_formats']):
+                        elif format_fallback and ctx['incomplete_formats']:
                             # for extractors with incomplete formats (audio only (soundcloud)
                             # or video only (imgur)) best/worst will fallback to
                             # best/worst {video,audio}-only format
-                            yield formats[format_idx]
+                            n = len(formats)
+                            if -n <= format_idx < n:
+                                yield formats[format_idx]
 
             elif selector.type == MERGE:        # +
                 def _merge(formats_pair):
diff --git a/yt_dlp/extractor/commonprotocols.py b/yt_dlp/extractor/commonprotocols.py
index d98331a4e..1fc7779b8 100644
--- a/yt_dlp/extractor/commonprotocols.py
+++ b/yt_dlp/extractor/commonprotocols.py
@@ -1,5 +1,7 @@
 from __future__ import unicode_literals
 
+import re
+
 from .common import InfoExtractor
 from ..compat import (
     compat_urlparse,
@@ -58,3 +60,16 @@ def _real_extract(self, url):
             'title': title,
             'url': url,
         }
+
+
+class ViewSourceIE(InfoExtractor):
+    IE_DESC = False
+    _VALID_URL = r'view-source:(?P<url>.+)'
+
+    _TEST = {
+        'url': 'view-source:https://www.youtube.com/watch?v=BaW_jenozKc',
+        'only_matching': True
+    }
+
+    def _real_extract(self, url):
+        return self.url_result(re.match(self._VALID_URL, url).group('url'))
diff --git a/yt_dlp/extractor/dplay.py b/yt_dlp/extractor/dplay.py
index bbb199094..e8da71dd6 100644
--- a/yt_dlp/extractor/dplay.py
+++ b/yt_dlp/extractor/dplay.py
@@ -296,6 +296,51 @@ def _real_extract(self, url):
             url, display_id, host, 'dplay' + country, country)
 
 
+class DiscoveryPlusIndiaIE(DPlayIE):
+    _VALID_URL = r'https?://(?:www\.)?discoveryplus\.in/videos?' + DPlayIE._PATH_REGEX
+    _TESTS = [{
+        'url': 'https://www.discoveryplus.in/videos/how-do-they-do-it/fugu-and-more?seasonId=8&type=EPISODE',
+        'info_dict': {
+            'id': '27104',
+            'ext': 'mp4',
+            'display_id': 'how-do-they-do-it/fugu-and-more',
+            'title': 'Fugu and More',
+            'description': 'The Japanese catch, prepare and eat the deadliest fish on the planet.',
+            'duration': 1319,
+            'timestamp': 1582309800,
+            'upload_date': '20200221',
+            'series': 'How Do They Do It?',
+            'season_number': 8,
+            'episode_number': 2,
+            'creator': 'Discovery Channel',
+        },
+        'params': {
+            'format': 'bestvideo',
+            'skip_download': True,
+        },
+        'skip': 'Cookies (not necessarily logged in) are needed'
+    }]
+
+    def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
+        headers['x-disco-params'] = 'realm=%s' % realm
+        headers['x-disco-client'] = 'WEB:UNKNOWN:dplus-india:17.0.0'
+
+    def _download_video_playback_info(self, disco_base, video_id, headers):
+        return self._download_json(
+            disco_base + 'playback/v3/videoPlaybackInfo',
+            video_id, headers=headers, data=json.dumps({
+                'deviceInfo': {
+                    'adBlocker': False,
+                },
+                'videoId': video_id,
+            }).encode('utf-8'))['data']['attributes']['streaming']
+
+    def _real_extract(self, url):
+        display_id = self._match_id(url)
+        return self._get_disco_api_info(
+            url, display_id, 'ap2-prod-direct.discoveryplus.in', 'dplusindia', 'in')
+
+
 class DiscoveryPlusIE(DPlayIE):
     _VALID_URL = r'https?://(?:www\.)?discoveryplus\.com/video' + DPlayIE._PATH_REGEX
     _TESTS = [{
diff --git a/yt_dlp/extractor/extractors.py b/yt_dlp/extractor/extractors.py
index 482d526ab..d9e6742ba 100644
--- a/yt_dlp/extractor/extractors.py
+++ b/yt_dlp/extractor/extractors.py
@@ -264,6 +264,7 @@
 from .commonprotocols import (
     MmsIE,
     RtmpIE,
+    ViewSourceIE,
 )
 from .condenast import CondeNastIE
 from .contv import CONtvIE
@@ -315,6 +316,7 @@
 from .dplay import (
     DPlayIE,
     DiscoveryPlusIE,
+    DiscoveryPlusIndiaIE,
     HGTVDeIE,
 )
 from .dreisat import DreiSatIE
diff --git a/yt_dlp/extractor/mildom.py b/yt_dlp/extractor/mildom.py
index 2e750d6d1..50daaf583 100644
--- a/yt_dlp/extractor/mildom.py
+++ b/yt_dlp/extractor/mildom.py
@@ -5,6 +5,7 @@
 import itertools
 import json
 import base64
+import re
 
 from .common import InfoExtractor
 from ..utils import (
@@ -68,7 +69,7 @@ def _fetch_dispatcher_config(self):
                 self._DISPATCHER_CONFIG = self._parse_json(base64.b64decode(tmp['data']), 'initialization')
             except ExtractorError:
                 self._DISPATCHER_CONFIG = self._download_json(
-                    'https://bookish-octo-barnacle.vercel.app/api/dispatcher_config', 'initialization',
+                    'https://bookish-octo-barnacle.vercel.app/api/mildom/dispatcher_config', 'initialization',
                     note='Downloading dispatcher_config fallback')
         return self._DISPATCHER_CONFIG
 
@@ -110,6 +111,7 @@ def _real_extract(self, url):
         enterstudio = self._call_api(
             'https://cloudac.mildom.com/nonolive/gappserv/live/enterstudio', video_id,
             note='Downloading live metadata', query={'user_id': video_id})
+        result_video_id = enterstudio.get('log_id', video_id)
 
         title = try_get(
             enterstudio, (
@@ -128,7 +130,7 @@ def _real_extract(self, url):
             ), compat_str)
 
         servers = self._call_api(
-            'https://cloudac.mildom.com/nonolive/gappserv/live/liveserver', video_id,
+            'https://cloudac.mildom.com/nonolive/gappserv/live/liveserver', result_video_id,
             note='Downloading live server list', query={
                 'user_id': video_id,
                 'live_server_type': 'hls',
@@ -139,7 +141,7 @@ def _real_extract(self, url):
             'is_lhls': '0',
         })
         m3u8_url = update_url_query(servers['stream_server'] + '/%s_master.m3u8' % video_id, stream_query)
-        formats = self._extract_m3u8_formats(m3u8_url, video_id, 'mp4', headers={
+        formats = self._extract_m3u8_formats(m3u8_url, result_video_id, 'mp4', headers={
             'Referer': 'https://www.mildom.com/',
             'Origin': 'https://www.mildom.com',
         }, note='Downloading m3u8 information')
@@ -150,13 +152,13 @@ def _real_extract(self, url):
             parsed = parsed._replace(
                 netloc='bookish-octo-barnacle.vercel.app',
                 query=compat_urllib_parse_urlencode(stream_query, True),
-                path='/api' + parsed.path)
+                path='/api/mildom' + parsed.path)
             fmt['url'] = compat_urlparse.urlunparse(parsed)
 
         self._sort_formats(formats)
 
         return {
-            'id': video_id,
+            'id': result_video_id,
             'title': title,
             'description': description,
             'uploader': uploader,
@@ -172,9 +174,8 @@ class MildomVodIE(MildomBaseIE):
     _VALID_URL = r'https?://(?:(?:www|m)\.)mildom\.com/playback/(?P<user_id>\d+)/(?P<id>(?P=user_id)-[a-zA-Z0-9]+)'
 
     def _real_extract(self, url):
-        video_id = self._match_id(url)
-        m = self._VALID_URL_RE.match(url)
-        user_id = m.group('user_id')
+        m = re.match(self._VALID_URL, url)
+        user_id, video_id = m.group('user_id'), m.group('id')
         url = 'https://www.mildom.com/playback/%s/%s' % (user_id, video_id)
 
         webpage = self._download_webpage(url, video_id)
@@ -230,7 +231,7 @@ def _real_extract(self, url):
             parsed = parsed._replace(
                 netloc='bookish-octo-barnacle.vercel.app',
                 query=compat_urllib_parse_urlencode(stream_query, True),
-                path='/api/vod2/proxy')
+                path='/api/mildom/vod2/proxy')
             fmt['url'] = compat_urlparse.urlunparse(parsed)
 
         self._sort_formats(formats)
diff --git a/yt_dlp/extractor/youtube.py b/yt_dlp/extractor/youtube.py
index 794f51bb6..3e19d18a0 100644
--- a/yt_dlp/extractor/youtube.py
+++ b/yt_dlp/extractor/youtube.py
@@ -2296,8 +2296,10 @@ def chapter_time(mmlir):
         is_private = bool_or_none(video_details.get('isPrivate'))
         is_unlisted = bool_or_none(microformat.get('isUnlisted'))
         is_membersonly = None
+        is_premium = None
         if initial_data and is_private is not None:
             is_membersonly = False
+            is_premium = False
             contents = try_get(initial_data, lambda x: x['contents']['twoColumnWatchNextResults']['results']['results']['contents'], list)
             for content in contents or []:
                 badges = try_get(content, lambda x: x['videoPrimaryInfoRenderer']['badges'], list)
@@ -2306,13 +2308,16 @@ def chapter_time(mmlir):
                     if label.lower() == 'members only':
                         is_membersonly = True
                         break
-                if is_membersonly:
+                    elif label.lower() == 'premium':
+                        is_premium = True
+                        break
+                if is_membersonly or is_premium:
                     break
 
         # TODO: Add this for playlists
         info['availability'] = self._availability(
             is_private=is_private,
-            needs_premium=False,  # Youtube no longer have premium-only videos?
+            needs_premium=is_premium,
             needs_subscription=is_membersonly,
             needs_auth=info['age_limit'] >= 18,
             is_unlisted=None if is_private is None else is_unlisted)
diff --git a/yt_dlp/options.py b/yt_dlp/options.py
index 2047ac28e..1057cb643 100644
--- a/yt_dlp/options.py
+++ b/yt_dlp/options.py
@@ -862,7 +862,7 @@ def _dict_from_multiple_values_options_callback(
         callback_kwargs={
             'allowed_keys': '|'.join(OUTTMPL_TYPES.keys()),
             'default_key': 'default', 'process': lambda x: x.strip()},
-        help='Output filename template, see "OUTPUT TEMPLATE" for details')
+        help='Output filename template; see "OUTPUT TEMPLATE" for details')
     filesystem.add_option(
         '--output-na-placeholder',
         dest='outtmpl_na_placeholder', metavar='TEXT', default='NA',
@@ -1144,22 +1144,10 @@ def _dict_from_multiple_values_options_callback(
         help=optparse.SUPPRESS_HELP)
     postproc.add_option(
         '--parse-metadata',
-        metavar='FIELD:FORMAT', dest='metafromfield', action='append',
+        metavar='FROM:TO', dest='metafromfield', action='append',
         help=(
-            'Parse additional metadata like title/artist from other fields. '
-            'Give a template or field name to extract data from and the '
-            'format to interpret it as, separated by a ":". '
-            'Either regular expression with named capture groups or a '
-            'similar syntax to the output template can be used for the FORMAT. '
-            'Similarly, the syntax for output template can be used for FIELD '
-            'to parse the data from multiple fields. '
-            'The parsed parameters replace any existing values and can be used in output templates. '
-            'This option can be used multiple times. '
-            'Example: --parse-metadata "title:%(artist)s - %(title)s" matches a title like '
-            '"Coldplay - Paradise". '
-            'Example: --parse-metadata "%(series)s %(episode_number)s:%(title)s" '
-            'sets the title using series and episode number. '
-            'Example (regex): --parse-metadata "description:Artist - (?P<artist>.+?)"'))
+            'Parse additional metadata like title/artist from other fields; '
+            'see "MODIFYING METADATA" for details'))
     postproc.add_option(
         '--xattrs',
         action='store_true', dest='xattrs', default=False,
diff --git a/yt_dlp/postprocessor/ffmpeg.py b/yt_dlp/postprocessor/ffmpeg.py
index ffce98cd5..accd715be 100644
--- a/yt_dlp/postprocessor/ffmpeg.py
+++ b/yt_dlp/postprocessor/ffmpeg.py
@@ -10,7 +10,7 @@
 
 from .common import AudioConversionError, PostProcessor
 
-from ..compat import compat_str
+from ..compat import compat_str, compat_numeric_types
 from ..utils import (
     encodeArgument,
     encodeFilename,
@@ -530,6 +530,8 @@ def run(self, info):
         metadata = {}
 
         def add(meta_list, info_list=None):
+            if not meta_list:
+                return
             if not info_list:
                 info_list = meta_list
             if not isinstance(meta_list, (list, tuple)):
@@ -537,7 +539,7 @@ def add(meta_list, info_list=None):
             if not isinstance(info_list, (list, tuple)):
                 info_list = (info_list,)
             for info_f in info_list:
-                if info.get(info_f) is not None:
+                if isinstance(info.get(info_f), (compat_str, compat_numeric_types)):
                     for meta_f in meta_list:
                         metadata[meta_f] = info[info_f]
                     break
@@ -563,6 +565,10 @@ def add(meta_list, info_list=None):
         add('episode_id', ('episode', 'episode_id'))
         add('episode_sort', 'episode_number')
 
+        prefix = 'meta_'
+        for key in filter(lambda k: k.startswith(prefix), info.keys()):
+            add(key[len(prefix):], key)
+
         if not metadata:
             self.to_screen('There isn\'t any metadata to add')
             return [], info
@@ -577,7 +583,7 @@ def add(meta_list, info_list=None):
         else:
             options.extend(['-c', 'copy'])
 
-        for (name, value) in metadata.items():
+        for name, value in metadata.items():
             options.extend(['-metadata', '%s=%s' % (name, value)])
 
         chapters = info.get('chapters', [])
diff --git a/yt_dlp/postprocessor/metadatafromfield.py b/yt_dlp/postprocessor/metadatafromfield.py
index 636199214..195c63f92 100644
--- a/yt_dlp/postprocessor/metadatafromfield.py
+++ b/yt_dlp/postprocessor/metadatafromfield.py
@@ -45,7 +45,7 @@ def format_to_regex(fmt):
         # replace %(..)s with regex group and escape other string parts
         for match in re.finditer(r'%\((\w+)\)s', fmt):
             regex += re.escape(fmt[lastpos:match.start()])
-            regex += r'(?P<%s>[^\r\n]+)' % match.group(1)
+            regex += r'(?P<%s>.+)' % match.group(1)
             lastpos = match.end()
         if lastpos < len(fmt):
             regex += re.escape(fmt[lastpos:])


Test patch:
diff --git a/test/test_postprocessors.py b/test/test_postprocessors.py
index 2e9a1bf94..12fc427fb 100644
--- a/test/test_postprocessors.py
+++ b/test/test_postprocessors.py
@@ -14,10 +14,10 @@
 class TestMetadataFromField(unittest.TestCase):
     def test_format_to_regex(self):
         pp = MetadataFromFieldPP(None, ['title:%(title)s - %(artist)s'])
-        self.assertEqual(pp._data[0]['regex'], r'(?P<title>[^\r\n]+)\ \-\ (?P<artist>[^\r\n]+)')
+        self.assertEqual(pp._data[0]['regex'], r'(?P<title>.+)\ \-\ (?P<artist>.+)')
 
 
 class TestMetadataFromTitle(unittest.TestCase):
     def test_format_to_regex(self):
         pp = MetadataFromTitlePP(None, '%(title)s - %(artist)s')
-        self.assertEqual(pp._titleregex, r'(?P<title>[^\r\n]+)\ \-\ (?P<artist>[^\r\n]+)')
+        self.assertEqual(pp._titleregex, r'(?P<title>.+)\ \-\ (?P<artist>.+)')