aboutsummaryrefslogtreecommitdiffstats
path: root/youtube_dlc
diff options
context:
space:
mode:
Diffstat (limited to 'youtube_dlc')
-rw-r--r--youtube_dlc/YoutubeDL.py61
-rw-r--r--youtube_dlc/__init__.py4
-rw-r--r--youtube_dlc/compat.py26
-rw-r--r--youtube_dlc/options.py21
-rw-r--r--youtube_dlc/utils.py81
5 files changed, 190 insertions, 3 deletions
diff --git a/youtube_dlc/YoutubeDL.py b/youtube_dlc/YoutubeDL.py
index ee6d74910..97e4f451f 100644
--- a/youtube_dlc/YoutubeDL.py
+++ b/youtube_dlc/YoutubeDL.py
@@ -51,6 +51,9 @@ from .utils import (
DEFAULT_OUTTMPL,
determine_ext,
determine_protocol,
+ DOT_DESKTOP_LINK_TEMPLATE,
+ DOT_URL_LINK_TEMPLATE,
+ DOT_WEBLOC_LINK_TEMPLATE,
DownloadError,
encode_compat_str,
encodeFilename,
@@ -61,6 +64,7 @@ from .utils import (
formatSeconds,
GeoRestrictedError,
int_or_none,
+ iri_to_uri,
ISO3166Utils,
locked_file,
make_HTTPS_handler,
@@ -84,6 +88,7 @@ from .utils import (
std_headers,
str_or_none,
subtitles_filename,
+ to_high_limit_path,
UnavailableVideoError,
url_basename,
version_tuple,
@@ -187,6 +192,11 @@ class YoutubeDL(object):
writeannotations: Write the video annotations to a .annotations.xml file
writethumbnail: Write the thumbnail image to a file
write_all_thumbnails: Write all thumbnail formats to files
+ writelink: Write an internet shortcut file, depending on the
+ current platform (.url/.webloc/.desktop)
+ writeurllink: Write a Windows internet shortcut file (.url)
+ writewebloclink: Write a macOS internet shortcut file (.webloc)
+ writedesktoplink: Write a Linux internet shortcut file (.desktop)
writesubtitles: Write the video subtitles to a file
writeautomaticsub: Write the automatically generated subtitles to a file
allsubtitles: Downloads all the subtitles of the video
@@ -1984,6 +1994,57 @@ class YoutubeDL(object):
self._write_thumbnails(info_dict, filename)
+ # Write internet shortcut files
+ url_link = webloc_link = desktop_link = False
+ if self.params.get('writelink', False):
+ if sys.platform == "darwin": # macOS.
+ webloc_link = True
+ elif sys.platform.startswith("linux"):
+ desktop_link = True
+ else: # if sys.platform in ['win32', 'cygwin']:
+ url_link = True
+ if self.params.get('writeurllink', False):
+ url_link = True
+ if self.params.get('writewebloclink', False):
+ webloc_link = True
+ if self.params.get('writedesktoplink', False):
+ desktop_link = True
+
+ if url_link or webloc_link or desktop_link:
+ if 'webpage_url' not in info_dict:
+ self.report_error('Cannot write internet shortcut file because the "webpage_url" field is missing in the media information')
+ return
+ ascii_url = iri_to_uri(info_dict['webpage_url'])
+
+ def _write_link_file(extension, template, newline, embed_filename):
+ linkfn = replace_extension(filename, extension, info_dict.get('ext'))
+ if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(linkfn)):
+ self.to_screen('[info] Internet shortcut is already present')
+ else:
+ try:
+ self.to_screen('[info] Writing internet shortcut to: ' + linkfn)
+ with io.open(encodeFilename(to_high_limit_path(linkfn)), 'w', encoding='utf-8', newline=newline) as linkfile:
+ template_vars = {'url': ascii_url}
+ if embed_filename:
+ template_vars['filename'] = linkfn[:-(len(extension) + 1)]
+ linkfile.write(template % template_vars)
+ except (OSError, IOError):
+ self.report_error('Cannot write internet shortcut ' + linkfn)
+ return False
+ return True
+
+ if url_link:
+ if not _write_link_file('url', DOT_URL_LINK_TEMPLATE, '\r\n', embed_filename=False):
+ return
+ if webloc_link:
+ if not _write_link_file('webloc', DOT_WEBLOC_LINK_TEMPLATE, '\n', embed_filename=False):
+ return
+ if desktop_link:
+ if not _write_link_file('desktop', DOT_DESKTOP_LINK_TEMPLATE, '\n', embed_filename=True):
+ return
+
+ # Download
+ must_record_download_archive = False
if not self.params.get('skip_download', False):
try:
if info_dict.get('requested_formats') is not None:
diff --git a/youtube_dlc/__init__.py b/youtube_dlc/__init__.py
index df07016e1..d183016b6 100644
--- a/youtube_dlc/__init__.py
+++ b/youtube_dlc/__init__.py
@@ -389,6 +389,10 @@ def _real_main(argv=None):
'writeinfojson': opts.writeinfojson,
'writethumbnail': opts.writethumbnail,
'write_all_thumbnails': opts.write_all_thumbnails,
+ 'writelink': opts.writelink,
+ 'writeurllink': opts.writeurllink,
+ 'writewebloclink': opts.writewebloclink,
+ 'writedesktoplink': opts.writedesktoplink,
'writesubtitles': opts.writesubtitles,
'writeautomaticsub': opts.writeautomaticsub,
'allsubtitles': opts.allsubtitles,
diff --git a/youtube_dlc/compat.py b/youtube_dlc/compat.py
index ac889ddd7..4a69b098f 100644
--- a/youtube_dlc/compat.py
+++ b/youtube_dlc/compat.py
@@ -38,14 +38,19 @@ except ImportError: # Python 2
import urllib as compat_urllib_parse
try:
+ import urllib.parse as compat_urlparse
+except ImportError: # Python 2
+ import urlparse as compat_urlparse
+
+try:
from urllib.parse import urlparse as compat_urllib_parse_urlparse
except ImportError: # Python 2
from urlparse import urlparse as compat_urllib_parse_urlparse
try:
- import urllib.parse as compat_urlparse
+ from urllib.parse import urlunparse as compat_urllib_parse_urlunparse
except ImportError: # Python 2
- import urlparse as compat_urlparse
+ from urlparse import urlunparse as compat_urllib_parse_urlunparse
try:
import urllib.response as compat_urllib_response
@@ -2366,6 +2371,20 @@ except NameError:
compat_str = str
try:
+ from urllib.parse import quote as compat_urllib_parse_quote
+ from urllib.parse import quote_plus as compat_urllib_parse_quote_plus
+except ImportError: # Python 2
+ def compat_urllib_parse_quote(string, safe='/'):
+ return compat_urllib_parse.quote(
+ string.encode('utf-8'),
+ str(safe))
+
+ def compat_urllib_parse_quote_plus(string, safe=''):
+ return compat_urllib_parse.quote_plus(
+ string.encode('utf-8'),
+ str(safe))
+
+try:
from urllib.parse import unquote_to_bytes as compat_urllib_parse_unquote_to_bytes
from urllib.parse import unquote as compat_urllib_parse_unquote
from urllib.parse import unquote_plus as compat_urllib_parse_unquote_plus
@@ -3033,11 +3052,14 @@ __all__ = [
'compat_tokenize_tokenize',
'compat_urllib_error',
'compat_urllib_parse',
+ 'compat_urllib_parse_quote',
+ 'compat_urllib_parse_quote_plus',
'compat_urllib_parse_unquote',
'compat_urllib_parse_unquote_plus',
'compat_urllib_parse_unquote_to_bytes',
'compat_urllib_parse_urlencode',
'compat_urllib_parse_urlparse',
+ 'compat_urllib_parse_urlunparse',
'compat_urllib_request',
'compat_urllib_request_DataHandler',
'compat_urllib_response',
diff --git a/youtube_dlc/options.py b/youtube_dlc/options.py
index 44eba3e9c..bd85abd3a 100644
--- a/youtube_dlc/options.py
+++ b/youtube_dlc/options.py
@@ -830,7 +830,25 @@ def parseOpts(overrideArguments=None):
action='store_true', dest='list_thumbnails', default=False,
help='Simulate and list all available thumbnail formats')
- postproc = optparse.OptionGroup(parser, 'Post-processing Options')
+ link = optparse.OptionGroup(parser, 'Internet Shortcut Options')
+ link.add_option(
+ '--write-link',
+ action='store_true', dest='writelink', default=False,
+ help='Write an internet shortcut file, depending on the current platform (.url/.webloc/.desktop). The URL may be cached by the OS.')
+ link.add_option(
+ '--write-url-link',
+ action='store_true', dest='writeurllink', default=False,
+ help='Write a Windows internet shortcut file (.url). Note that the OS caches the URL based on the file path.')
+ link.add_option(
+ '--write-webloc-link',
+ action='store_true', dest='writewebloclink', default=False,
+ help='Write a macOS internet shortcut file (.webloc)')
+ link.add_option(
+ '--write-desktop-link',
+ action='store_true', dest='writedesktoplink', default=False,
+ help='Write a Linux internet shortcut file (.desktop)')
+
+ postproc = optparse.OptionGroup(parser, 'Post-Processing Options')
postproc.add_option(
'-x', '--extract-audio',
action='store_true', dest='extractaudio', default=False,
@@ -932,6 +950,7 @@ def parseOpts(overrideArguments=None):
parser.add_option_group(downloader)
parser.add_option_group(filesystem)
parser.add_option_group(thumbnail)
+ parser.add_option_group(link)
parser.add_option_group(verbosity)
parser.add_option_group(workarounds)
parser.add_option_group(video_format)
diff --git a/youtube_dlc/utils.py b/youtube_dlc/utils.py
index 68b4ca944..d814eb2ac 100644
--- a/youtube_dlc/utils.py
+++ b/youtube_dlc/utils.py
@@ -60,6 +60,9 @@ from .compat import (
compat_urllib_parse,
compat_urllib_parse_urlencode,
compat_urllib_parse_urlparse,
+ compat_urllib_parse_urlunparse,
+ compat_urllib_parse_quote,
+ compat_urllib_parse_quote_plus,
compat_urllib_parse_unquote_plus,
compat_urllib_request,
compat_urlparse,
@@ -5714,3 +5717,81 @@ def random_birthday(year_field, month_field, day_field):
month_field: str(random_date.month),
day_field: str(random_date.day),
}
+
+# Templates for internet shortcut files, which are plain text files.
+DOT_URL_LINK_TEMPLATE = '''
+[InternetShortcut]
+URL=%(url)s
+'''.lstrip()
+
+DOT_WEBLOC_LINK_TEMPLATE = '''
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+\t<key>URL</key>
+\t<string>%(url)s</string>
+</dict>
+</plist>
+'''.lstrip()
+
+DOT_DESKTOP_LINK_TEMPLATE = '''
+[Desktop Entry]
+Encoding=UTF-8
+Name=%(filename)s
+Type=Link
+URL=%(url)s
+Icon=text-html
+'''.lstrip()
+
+
+def iri_to_uri(iri):
+ """
+ Converts an IRI (Internationalized Resource Identifier, allowing Unicode characters) to a URI (Uniform Resource Identifier, ASCII-only).
+
+ The function doesn't add an additional layer of escaping; e.g., it doesn't escape `%3C` as `%253C`. Instead, it percent-escapes characters with an underlying UTF-8 encoding *besides* those already escaped, leaving the URI intact.
+ """
+
+ iri_parts = compat_urllib_parse_urlparse(iri)
+
+ if '[' in iri_parts.netloc:
+ raise ValueError('IPv6 URIs are not, yet, supported.')
+ # Querying `.netloc`, when there's only one bracket, also raises a ValueError.
+
+ # The `safe` argument values, that the following code uses, contain the characters that should not be percent-encoded. Everything else but letters, digits and '_.-' will be percent-encoded with an underlying UTF-8 encoding. Everything already percent-encoded will be left as is.
+
+ net_location = ''
+ if iri_parts.username:
+ net_location += compat_urllib_parse_quote(iri_parts.username, safe=r"!$%&'()*+,~")
+ if iri_parts.password is not None:
+ net_location += ':' + compat_urllib_parse_quote(iri_parts.password, safe=r"!$%&'()*+,~")
+ net_location += '@'
+
+ net_location += iri_parts.hostname.encode('idna').decode('utf-8') # Punycode for Unicode hostnames.
+ # The 'idna' encoding produces ASCII text.
+ if iri_parts.port is not None and iri_parts.port != 80:
+ net_location += ':' + str(iri_parts.port)
+
+ return compat_urllib_parse_urlunparse(
+ (iri_parts.scheme,
+ net_location,
+
+ compat_urllib_parse_quote_plus(iri_parts.path, safe=r"!$%&'()*+,/:;=@|~"),
+
+ # Unsure about the `safe` argument, since this is a legacy way of handling parameters.
+ compat_urllib_parse_quote_plus(iri_parts.params, safe=r"!$%&'()*+,/:;=@|~"),
+
+ # Not totally sure about the `safe` argument, since the source does not explicitly mention the query URI component.
+ compat_urllib_parse_quote_plus(iri_parts.query, safe=r"!$%&'()*+,/:;=?@{|}~"),
+
+ compat_urllib_parse_quote_plus(iri_parts.fragment, safe=r"!#$%&'()*+,/:;=?@{|}~")))
+
+ # Source for `safe` arguments: https://url.spec.whatwg.org/#percent-encoded-bytes.
+
+
+def to_high_limit_path(path):
+ if sys.platform in ['win32', 'cygwin']:
+ # Work around MAX_PATH limitation on Windows. The maximum allowed length for the individual path segments may still be quite limited.
+ return r'\\?\ '.rstrip() + os.path.abspath(path)
+
+ return path