diff --git a/Contents/Code/constants.py b/Contents/Code/constants.py index 2eb85846..a5cddfcd 100644 --- a/Contents/Code/constants.py +++ b/Contents/Code/constants.py @@ -39,7 +39,6 @@ plugin_support_directory = os.path.join(app_support_directory, 'Plug-in Support') plugin_support_data_directory = os.path.join(plugin_support_directory, 'Data') themerr_data_directory = os.path.join(plugin_support_data_directory, plugin_identifier, 'DataItems') -cookie_jar_file = os.path.join(plugin_support_data_directory, plugin_identifier, 'HTTPCookies') contributes_to = [ 'tv.plex.agents.movie', diff --git a/Contents/Code/default_prefs.py b/Contents/Code/default_prefs.py index c8b4dc03..bfcb1e1d 100644 --- a/Contents/Code/default_prefs.py +++ b/Contents/Code/default_prefs.py @@ -17,6 +17,7 @@ int_plexapi_upload_threads='3', str_youtube_user='', str_youtube_passwd='', + str_youtube_cookies='', enum_webapp_locale='en', str_webapp_http_host='0.0.0.0', int_webapp_http_port='9494', diff --git a/Contents/Code/youtube_dl_helper.py b/Contents/Code/youtube_dl_helper.py index 997a14f4..f493b626 100644 --- a/Contents/Code/youtube_dl_helper.py +++ b/Contents/Code/youtube_dl_helper.py @@ -2,6 +2,9 @@ # standard imports import logging +import json +import os +import tempfile # plex debugging try: @@ -13,7 +16,7 @@ from plexhints.prefs_kit import Prefs # prefs kit # imports from Libraries\Shared -from constants import plugin_identifier, cookie_jar_file +from constants import plugin_identifier, plugin_support_data_directory from typing import Optional import youtube_dl @@ -21,6 +24,24 @@ plugin_logger = logging.getLogger(plugin_identifier) +def nsbool(value): + # type: (bool) -> str + """ + Format a boolean value for a Netscape cookie jar file. + + Parameters + ---------- + value : bool + The boolean value to format. + + Returns + ------- + str + 'TRUE' or 'FALSE'. + """ + return 'TRUE' if value else 'FALSE' + + def process_youtube(url): # type: (str) -> Optional[str] """ @@ -41,8 +62,12 @@ def process_youtube(url): >>> process_youtube(url='https://www.youtube.com/watch?v=dQw4w9WgXcQ') ... """ + + cookie_jar_file = tempfile.NamedTemporaryFile(dir=plugin_support_data_directory, delete=False) + cookie_jar_file.write('# Netscape HTTP Cookie File\n') + youtube_dl_params = dict( - cookiefile=cookie_jar_file, + cookiefile=cookie_jar_file.name, logger=plugin_logger, outtmpl=u'%(id)s.%(ext)s', password=Prefs['str_youtube_passwd'] if Prefs['str_youtube_passwd'] else None, @@ -51,64 +76,92 @@ def process_youtube(url): youtube_include_dash_manifest=False, ) - ydl = youtube_dl.YoutubeDL(params=youtube_dl_params) - - with ydl: + if Prefs['str_youtube_cookies']: try: - result = ydl.extract_info( - url=url, - download=False # We just want to extract the info - ) - except Exception as exc: - if isinstance(exc, youtube_dl.utils.ExtractorError) and exc.expected: - Log.Info('YDL returned YT error while downloading %s: %s' % (url, exc)) - else: - Log.Exception('YDL returned an unexpected error while downloading %s: %s' % (url, exc)) - return None - - if 'entries' in result: - # Can be a playlist or a list of videos - video_data = result['entries'][0] - else: - # Just a video - video_data = result - - selected = { - 'opus': { - 'size': 0, - 'audio_url': None - }, - 'mp4a': { - 'size': 0, - 'audio_url': None - }, - } - if video_data: - for fmt in video_data['formats']: # loop through formats, select largest audio size for better quality - if 'audio only' in fmt['format']: - if 'opus' == fmt['acodec']: - temp_codec = 'opus' - elif 'mp4a' == fmt['acodec'].split('.')[0]: - temp_codec = 'mp4a' + cookies = json.loads(Prefs['str_youtube_cookies']) + for cookie in cookies: + include_subdom = cookie['domain'].startswith('.') + expiry = int(cookie.get('expiry', 0)) + values = [ + cookie['domain'], + nsbool(include_subdom), + cookie['path'], + nsbool(cookie['secure']), + str(expiry), + cookie['name'], + cookie['value'] + ] + cookie_jar_file.write('{}\n'.format('\t'.join(values))) + except Exception as e: + Log.Exception('Failed to write YouTube cookies to file, will try anyway. Error: {}'.format(e)) + + cookie_jar_file.flush() + cookie_jar_file.close() + + try: + ydl = youtube_dl.YoutubeDL(params=youtube_dl_params) + + with ydl: + try: + result = ydl.extract_info( + url=url, + download=False # We just want to extract the info + ) + except Exception as exc: + if isinstance(exc, youtube_dl.utils.ExtractorError) and exc.expected: + Log.Info('YDL returned YT error while downloading {}: {}'.format(url, exc)) else: - Log.Debug('Unknown codec: %s' % fmt['acodec']) - continue # unknown codec - filesize = int(fmt['filesize']) - if filesize > selected[temp_codec]['size']: - selected[temp_codec]['size'] = filesize - selected[temp_codec]['audio_url'] = fmt['url'] - - audio_url = None - - if 0 < selected['opus']['size'] > selected['mp4a']['size']: - audio_url = selected['opus']['audio_url'] - elif 0 < selected['mp4a']['size'] > selected['opus']['size']: - audio_url = selected['mp4a']['audio_url'] - - if audio_url and Prefs['bool_prefer_mp4a_codec']: # mp4a codec is preferred - if selected['mp4a']['audio_url']: # mp4a codec is available - audio_url = selected['mp4a']['audio_url'] - elif selected['opus']['audio_url']: # fallback to opus :( + Log.Exception('YDL returned an unexpected error while downloading {}: {}'.format(url, exc)) + return None + + if 'entries' in result: + # Can be a playlist or a list of videos + video_data = result['entries'][0] + else: + # Just a video + video_data = result + + selected = { + 'opus': { + 'size': 0, + 'audio_url': None + }, + 'mp4a': { + 'size': 0, + 'audio_url': None + }, + } + if video_data: + for fmt in video_data['formats']: # loop through formats, select largest audio size for better quality + if 'audio only' in fmt['format']: + if 'opus' == fmt['acodec']: + temp_codec = 'opus' + elif 'mp4a' == fmt['acodec'].split('.')[0]: + temp_codec = 'mp4a' + else: + Log.Debug('Unknown codec: %s' % fmt['acodec']) + continue # unknown codec + filesize = int(fmt['filesize']) + if filesize > selected[temp_codec]['size']: + selected[temp_codec]['size'] = filesize + selected[temp_codec]['audio_url'] = fmt['url'] + + audio_url = None + + if 0 < selected['opus']['size'] > selected['mp4a']['size']: audio_url = selected['opus']['audio_url'] + elif 0 < selected['mp4a']['size'] > selected['opus']['size']: + audio_url = selected['mp4a']['audio_url'] - return audio_url # return None or url found + if audio_url and Prefs['bool_prefer_mp4a_codec']: # mp4a codec is preferred + if selected['mp4a']['audio_url']: # mp4a codec is available + audio_url = selected['mp4a']['audio_url'] + elif selected['opus']['audio_url']: # fallback to opus :( + audio_url = selected['opus']['audio_url'] + + return audio_url # return None or url found + finally: + try: + os.remove(cookie_jar_file.name) + except Exception as e: + Log.Exception('Failed to delete cookie jar file: {}'.format(e)) diff --git a/Contents/DefaultPrefs.json b/Contents/DefaultPrefs.json index 90e77c69..a3904c81 100644 --- a/Contents/DefaultPrefs.json +++ b/Contents/DefaultPrefs.json @@ -112,6 +112,13 @@ "option": "hidden", "secure": "true" }, + { + "id": "str_youtube_cookies", + "type": "text", + "label": "YouTube Cookies (JSON format)", + "default": "", + "secure": "true" + }, { "id": "enum_webapp_locale", "type": "enum", diff --git a/docs/source/about/troubleshooting.rst b/docs/source/about/troubleshooting.rst index 079a44ba..baceb0a4 100644 --- a/docs/source/about/troubleshooting.rst +++ b/docs/source/about/troubleshooting.rst @@ -13,9 +13,12 @@ Adding your YouTube credentials (e-mail and password) in Themerr's preference ma YouTube also sometimes changes the way its login page works, preventing YouTube-DL from using those credentials. A workaround is to login in a web browser, and then export your YouTube cookies with a tool such as `Get cookies.txt -locally `__. You -should then add the exported cookies to the ``HTTPCookies`` file that can be found in the -``Plug-in Support/Data/dev.lizardbyte.themerr-plex/`` directory, starting from the Plex data root. +locally `__. Note +that Themerr currently only supports Chromium's JSON export format. In the exporter you use, if prompted, you need to +use the "JSON" or "Chrome" format. + +You can then paste that value in the "YouTube Cookies" field in the plugin preferences page. On the next media update +or scheduled run, the cookies will be used and hopefully videos will start downloading again. Plugin Logs ----------- diff --git a/docs/source/about/usage.rst b/docs/source/about/usage.rst index 60d781f5..0e8b1365 100644 --- a/docs/source/about/usage.rst +++ b/docs/source/about/usage.rst @@ -230,6 +230,16 @@ Description Default None +YouTube Cookies +^^^^^^^^^^^^^^^^ + +Description + The cookies to use for the requests to YouTube. Should be in Chromium JSON export format. + `Example exporter `__. + +Default + None + Web UI Locale ^^^^^^^^^^^^^