Repository: yt-dlp/yt-dlp

Issue:
[Feature Request] Add Date Option Patch
<!--

######################################################################
  WARNING!
  IGNORING THE FOLLOWING TEMPLATE WILL RESULT IN ISSUE CLOSED AS INCOMPLETE
######################################################################

-->


## Checklist

<!--
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of youtube-dlc:
- First of, make sure you are using the latest version of yt-dlp. Run `youtube-dlc --version` and ensure your version is 2021.01.29. If it's not, see https://github.com/pukkandan/yt-dlp on how to update. Issues with outdated version will be REJECTED.
- Search the bugtracker for similar feature requests: https://github.com/pukkandan/yt-dlp. DO NOT post duplicates.
- Finally, put x into all relevant boxes like this [x] (Dont forget to delete the empty space)
-->

- [x] I'm reporting a feature request
- [x] I've verified that I'm running yt-dlp version **2021.01.29**
- [x] I've searched the bugtracker for similar feature requests including closed ones


## Description

<!--
Provide an explanation of your issue in an arbitrary form. Please make sure the description is worded well enough to be understood, see https://github.com/ytdl-org/youtube-dl#is-the-description-of-the-issue-itself-sufficient. Provide any additional information, suggested solution and as much context and examples as possible.
-->

There's a fork of youtube-dl by @kade-robertson with a great date option patch, which allows you to add the time/date when the video was uploaded to the filename:
https://github.com/kade-robertson/youtube-dl

Here's the relevant commit/patch:
https://github.com/ytdl-org/youtube-dl/commit/4d6c8b13eb362fa0dba39d8a647cd9697b79cfd9

It would be great if it could get merged in here. It's very useful.


Discussion:
The patch given is a temporary measure at best. What if someone else wants this functionality for `release_date` instead? Adding all those extra fields only makes the code bloated and hard to maintain in future. I will see if it is possible to create a general solution for this using the output template itself. Maybe something like `-o %(upload_date|yy-mm-dd)t`

Resolving patch:
diff --git a/youtube_dlc/YoutubeDL.py b/youtube_dlc/YoutubeDL.py
--- a/youtube_dlc/YoutubeDL.py
+++ b/youtube_dlc/YoutubeDL.py
@@ -61,6 +61,7 @@
     ExistingVideoReached,
     expand_path,
     ExtractorError,
+    float_or_none,
     format_bytes,
     format_field,
     formatSeconds,
@@ -91,6 +92,7 @@
     sanitized_Request,
     std_headers,
     str_or_none,
+    strftime_or_none,
     subtitles_filename,
     to_high_limit_path,
     UnavailableVideoError,
@@ -735,6 +737,11 @@ def prepare_filename(self, info_dict, warn=False):
         try:
             template_dict = dict(info_dict)
 
+            template_dict['duration_string'] = (  # %(duration>%H-%M-%S)s is wrong if duration > 24hrs
+                formatSeconds(info_dict['duration'], '-')
+                if info_dict.get('duration', None) is not None
+                else None)
+
             template_dict['epoch'] = int(time.time())
             autonumber_size = self.params.get('autonumber_size')
             if autonumber_size is None:
@@ -755,7 +762,8 @@ def prepare_filename(self, info_dict, warn=False):
             template_dict = dict((k, v if isinstance(v, compat_numeric_types) else sanitize(k, v))
                                  for k, v in template_dict.items()
                                  if v is not None and not isinstance(v, (list, tuple, dict)))
-            template_dict = collections.defaultdict(lambda: self.params.get('outtmpl_na_placeholder', 'NA'), template_dict)
+            na = self.params.get('outtmpl_na_placeholder', 'NA')
+            template_dict = collections.defaultdict(lambda: na, template_dict)
 
             outtmpl = self.params.get('outtmpl', DEFAULT_OUTTMPL)
 
@@ -773,27 +781,45 @@ def prepare_filename(self, info_dict, warn=False):
                     r'%%(\1)0%dd' % field_size_compat_map[mobj.group('field')],
                     outtmpl)
 
+            # As of [1] format syntax is:
+            #  %[mapping_key][conversion_flags][minimum_width][.precision][length_modifier]type
+            # 1. https://docs.python.org/2/library/stdtypes.html#string-formatting
+            FORMAT_RE = r'''(?x)
+                (?<!%)
+                %
+                \({0}\)  # mapping key
+                (?:[#0\-+ ]+)?  # conversion flags (optional)
+                (?:\d+)?  # minimum field width (optional)
+                (?:\.\d+)?  # precision (optional)
+                [hlL]?  # length modifier (optional)
+                (?P<type>[diouxXeEfFgGcrs%])  # conversion type
+            '''
+
+            numeric_fields = list(self._NUMERIC_FIELDS)
+
+            # Format date
+            FORMAT_DATE_RE = FORMAT_RE.format(r'(?P<key>(?P<field>\w+)>(?P<format>.+?))')
+            for mobj in re.finditer(FORMAT_DATE_RE, outtmpl):
+                conv_type, field, frmt, key = mobj.group('type', 'field', 'format', 'key')
+                if key in template_dict:
+                    continue
+                value = strftime_or_none(template_dict.get(field), frmt, na)
+                if conv_type in 'crs':  # string
+                    value = sanitize(field, value)
+                else:  # number
+                    numeric_fields.append(key)
+                    value = float_or_none(value, default=None)
+                if value is not None:
+                    template_dict[key] = value
+
             # Missing numeric fields used together with integer presentation types
             # in format specification will break the argument substitution since
             # string NA placeholder is returned for missing fields. We will patch
             # output template for missing fields to meet string presentation type.
-            for numeric_field in self._NUMERIC_FIELDS:
+            for numeric_field in numeric_fields:
                 if numeric_field not in template_dict:
-                    # As of [1] format syntax is:
-                    #  %[mapping_key][conversion_flags][minimum_width][.precision][length_modifier]type
-                    # 1. https://docs.python.org/2/library/stdtypes.html#string-formatting
-                    FORMAT_RE = r'''(?x)
-                        (?<!%)
-                        %
-                        \({0}\)  # mapping key
-                        (?:[#0\-+ ]+)?  # conversion flags (optional)
-                        (?:\d+)?  # minimum field width (optional)
-                        (?:\.\d+)?  # precision (optional)
-                        [hlL]?  # length modifier (optional)
-                        [diouxXeEfFgGcrs%]  # conversion type
-                    '''
                     outtmpl = re.sub(
-                        FORMAT_RE.format(numeric_field),
+                        FORMAT_RE.format(re.escape(numeric_field)),
                         r'%({0})s'.format(numeric_field), outtmpl)
 
             # expand_path translates '%%' into '%' and '$$' into '$'
@@ -996,10 +1022,6 @@ def add_default_extra_info(self, ie_result, ie, url):
         self.add_extra_info(ie_result, {
             'extractor': ie.IE_NAME,
             'webpage_url': url,
-            'duration_string': (
-                formatSeconds(ie_result['duration'], '-')
-                if ie_result.get('duration', None) is not None
-                else None),
             'webpage_url_basename': url_basename(url),
             'extractor_key': ie.ie_key(),
         })
diff --git a/youtube_dlc/utils.py b/youtube_dlc/utils.py
--- a/youtube_dlc/utils.py
+++ b/youtube_dlc/utils.py
@@ -50,6 +50,7 @@
     compat_html_entities_html5,
     compat_http_client,
     compat_integer_types,
+    compat_numeric_types,
     compat_kwargs,
     compat_os_name,
     compat_parse_qs,
@@ -3673,6 +3674,18 @@ def url_or_none(url):
     return url if re.match(r'^(?:(?:https?|rt(?:m(?:pt?[es]?|fp)|sp[su]?)|mms|ftps?):)?//', url) else None
 
 
+def strftime_or_none(timestamp, date_format, default=None):
+    datetime_object = None
+    try:
+        if isinstance(timestamp, compat_numeric_types):  # unix timestamp
+            datetime_object = datetime.datetime.utcfromtimestamp(timestamp)
+        elif isinstance(timestamp, compat_str):  # assume YYYYMMDD
+            datetime_object = datetime.datetime.strptime(timestamp, '%Y%m%d')
+        return datetime_object.strftime(date_format)
+    except (ValueError, TypeError, AttributeError):
+        return default
+
+
 def parse_duration(s):
     if not isinstance(s, compat_basestring):
         return None


Test patch: