18 Commits

Author SHA1 Message Date
Jesus
a6ca011202 version v0.3.0 2025-03-08 16:28:39 -05:00
Jesus
114c2572a4 Renew plyr UI and simplify elements 2025-03-08 16:28:27 -05:00
f64b362603 update logic plyr-start.js 2025-03-03 08:20:41 +08:00
2fd7910194 version 0.2.21 2025-03-02 06:24:03 +08:00
c2e53072f7 update dependencies 2025-03-01 04:58:31 +08:00
c2986f3b14 Refactoring get_app_version 2025-03-01 04:06:11 +08:00
57854169f4 minor fix deprecation warning
tests/test_util.py: 14 warnings
  /home/runner/work/yt-local/youtube-local/youtube/util.py:321: DeprecationWarning: HTTPResponse.getheader() is deprecated and will be removed in urllib3 v2.1.0. Instead use HTTPResponse.headers.get(name, default).
    response.getheader('Content-Encoding', default='identity'))
2025-03-01 01:12:09 +08:00
3217305f9f version 0.2.20 2025-02-28 11:04:06 +08:00
639aadd2c1 Remove gather_googlevideo_domains setting
This was an old experiment to collect googlevideo domains to see
if there was a pattern that could correlate to IP address to
look for workarounds for 403 errors

Can bug out if enabled and if failed to get any vidoe urls,
so remove since it is obsolete and some people are enabling it

See #218
2025-02-28 10:58:29 +08:00
7157df13cd Remove params to fetch_player_response 2025-02-28 10:58:15 +08:00
630e0137e0 Increase playlist count to 1000 by default if cannot get video count
This way, buttons will still appear even if there is a failure
to read playlist metadata

Fixes #220# Please enter the commit message for your changes. Lines starting
2025-02-28 10:51:51 +08:00
a0c51731af channel.py: Catch FetchError
Should catch this error to fail gracefully

See #227
2025-02-28 10:51:29 +08:00
d361996fc0 util: use visitorData for api request
watch: use android_vr client to get player data
2025-02-28 10:43:14 +08:00
Jesus
4ef7dda14a version 0.2.19 2024-10-11 11:25:12 +08:00
Jesus
ee31cedae0 Revert "Refactoring code and reuse INNERTUBE_CLIENTS"
This reverts commit 8af98968dd.
2024-10-11 11:22:36 +08:00
d3b0cb5e13 workflows: update git sync actions 2024-08-05 09:23:38 +08:00
0a79974d11 Add sync to c.fridu.us and sourcehut 2024-08-05 05:27:58 +08:00
4e327944a0 Add CI 2024-07-15 10:39:00 +08:00
14 changed files with 343 additions and 225 deletions

23
.gitea/workflows/ci.yaml Normal file
View File

@@ -0,0 +1,23 @@
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.11
- name: Install dependencies
run: |
pip install --upgrade pip
pip install -r requirements-dev.txt
- name: Run tests
run: pytest

View File

@@ -0,0 +1,40 @@
name: git-sync-with-mirror
on:
push:
branches: [ master ]
workflow_dispatch:
jobs:
git-sync:
runs-on: ubuntu-latest
steps:
- name: git-sync
env:
git_sync_source_repo: git@git.fridu.us:heckyel/yt-local.git
git_sync_destination_repo: ssh://git@c.fridu.us/software/yt-local.git
if: env.git_sync_source_repo && env.git_sync_destination_repo
uses: astounds/git-sync@v1
with:
source_repo: git@git.fridu.us:heckyel/yt-local.git
source_branch: "master"
destination_repo: ssh://git@c.fridu.us/software/yt-local.git
destination_branch: "master"
source_ssh_private_key: ${{ secrets.GIT_SYNC_SOURCE_SSH_PRIVATE_KEY }}
destination_ssh_private_key: ${{ secrets.GIT_SYNC_DESTINATION_SSH_PRIVATE_KEY }}
- name: git-sync-sourcehut
env:
git_sync_source_repo: git@git.fridu.us:heckyel/yt-local.git
git_sync_destination_repo: git@git.sr.ht:~heckyel/yt-local
if: env.git_sync_source_repo && env.git_sync_destination_repo
uses: astounds/git-sync@v1
with:
source_repo: git@git.fridu.us:heckyel/yt-local.git
source_branch: "master"
destination_repo: git@git.sr.ht:~heckyel/yt-local
destination_branch: "master"
source_ssh_private_key: ${{ secrets.GIT_SYNC_SOURCE_SSH_PRIVATE_KEY }}
destination_ssh_private_key: ${{ secrets.GIT_SYNC_DESTINATION_SSH_PRIVATE_KEY }}
continue-on-error: true

View File

@@ -1,21 +1,5 @@
blinker==1.7.0 # Include all production requirements
Brotli==1.1.0 -r requirements.txt
cachetools==5.3.3
click==8.1.7 # Development requirements
defusedxml==0.7.1 pytest>=6.2.1
Flask==3.0.2
gevent==24.2.1
greenlet==3.0.3
iniconfig==2.0.0
itsdangerous==2.1.2
Jinja2==3.1.4
MarkupSafe==2.1.5
packaging==24.0
pluggy==1.4.0
PySocks==1.7.1
pytest==8.1.1
stem==1.8.2
urllib3==2.2.2
Werkzeug==3.0.3
zope.event==5.0
zope.interface==6.2

View File

@@ -1,17 +1,8 @@
blinker==1.7.0 Flask>=1.0.3
Brotli==1.1.0 gevent>=1.2.2
cachetools==5.3.3 Brotli>=1.0.7
click==8.1.7 PySocks>=1.6.8
defusedxml==0.7.1 urllib3>=1.24.1
Flask==3.0.2 defusedxml>=0.5.0
gevent==24.2.1 cachetools>=4.0.0
greenlet==3.0.3 stem>=1.8.0
itsdangerous==2.1.2
Jinja2==3.1.4
MarkupSafe==2.1.5
PySocks==1.7.1
stem==1.8.2
urllib3==2.2.2
Werkzeug==3.0.3
zope.event==5.0
zope.interface==6.2

View File

@@ -322,13 +322,6 @@ Archive: https://archive.ph/OZQbN''',
'comment': '', 'comment': '',
}), }),
('gather_googlevideo_domains', {
'type': bool,
'default': False,
'comment': '''Developer use to debug 403s''',
'hidden': True,
}),
('debugging_save_responses', { ('debugging_save_responses', {
'type': bool, 'type': bool,
'default': False, 'default': False,
@@ -338,7 +331,7 @@ Archive: https://archive.ph/OZQbN''',
('settings_version', { ('settings_version', {
'type': int, 'type': int,
'default': 5, 'default': 6,
'comment': '''Do not change, remove, or comment out this value, or else your settings may be lost or corrupted''', 'comment': '''Do not change, remove, or comment out this value, or else your settings may be lost or corrupted''',
'hidden': True, 'hidden': True,
}), }),
@@ -419,11 +412,20 @@ def upgrade_to_5(settings_dict):
return new_settings return new_settings
def upgrade_to_6(settings_dict):
new_settings = settings_dict.copy()
if 'gather_googlevideo_domains' in new_settings:
del new_settings['gather_googlevideo_domains']
new_settings['settings_version'] = 6
return new_settings
upgrade_functions = { upgrade_functions = {
1: upgrade_to_2, 1: upgrade_to_2,
2: upgrade_to_3, 2: upgrade_to_3,
3: upgrade_to_4, 3: upgrade_to_4,
4: upgrade_to_5, 4: upgrade_to_5,
5: upgrade_to_6,
} }

View File

@@ -121,7 +121,7 @@ def error_page(e):
elif (exc_info()[0] == util.FetchError elif (exc_info()[0] == util.FetchError
and exc_info()[1].code == '404' and exc_info()[1].code == '404'
): ):
error_message = ('Error: The page you are looking for isn\'t here. ¯\_(ツ)_/¯') error_message = ('Error: The page you are looking for isn\'t here.')
return flask.render_template('error.html', return flask.render_template('error.html',
error_code=exc_info()[1].code, error_code=exc_info()[1].code,
error_message=error_message, error_message=error_message,

View File

@@ -292,7 +292,7 @@ def get_number_of_videos_channel(channel_id):
try: try:
response = util.fetch_url(url, headers_mobile, response = util.fetch_url(url, headers_mobile,
debug_name='number_of_videos', report_text='Got number of videos') debug_name='number_of_videos', report_text='Got number of videos')
except urllib.error.HTTPError as e: except (urllib.error.HTTPError, util.FetchError) as e:
traceback.print_exc() traceback.print_exc()
print("Couldn't retrieve number of videos") print("Couldn't retrieve number of videos")
return 1000 return 1000

View File

@@ -11,17 +11,10 @@ import subprocess
def app_version(): def app_version():
def minimal_env_cmd(cmd): def minimal_env_cmd(cmd):
# make minimal environment # make minimal environment
env = {} env = {k: os.environ[k] for k in ['SYSTEMROOT', 'PATH'] if k in os.environ}
for k in ['SYSTEMROOT', 'PATH']: env.update({'LANGUAGE': 'C', 'LANG': 'C', 'LC_ALL': 'C'})
v = os.environ.get(k)
if v is not None:
env[k] = v
env['LANGUAGE'] = 'C' out = subprocess.Popen(cmd, stdout=subprocess.PIPE, env=env).communicate()[0]
env['LANG'] = 'C'
env['LC_ALL'] = 'C'
out = subprocess.Popen(
cmd, stdout=subprocess.PIPE, env=env).communicate()[0]
return out return out
subst_list = { subst_list = {
@@ -31,24 +24,21 @@ def app_version():
} }
if os.system("command -v git > /dev/null 2>&1") != 0: if os.system("command -v git > /dev/null 2>&1") != 0:
subst_list return subst_list
else:
if call(["git", "branch"], stderr=STDOUT,
stdout=open(os.devnull, 'w')) != 0:
subst_list
else:
# version
describe = minimal_env_cmd(["git", "describe", "--always"])
git_revision = describe.strip().decode('ascii')
# branch
branch = minimal_env_cmd(["git", "branch"])
git_branch = branch.strip().decode('ascii').replace('* ', '')
subst_list = { if call(["git", "branch"], stderr=STDOUT, stdout=open(os.devnull, 'w')) != 0:
"version": __version__, return subst_list
"branch": git_branch,
"commit": git_revision describe = minimal_env_cmd(["git", "describe", "--tags", "--always"])
} git_revision = describe.strip().decode('ascii')
branch = minimal_env_cmd(["git", "branch"])
git_branch = branch.strip().decode('ascii').replace('* ', '')
subst_list.update({
"branch": git_branch,
"commit": git_revision
})
return subst_list return subst_list

View File

@@ -115,7 +115,7 @@ def get_playlist_page():
video_count = yt_data_extract.deep_get(info, 'metadata', 'video_count') video_count = yt_data_extract.deep_get(info, 'metadata', 'video_count')
if video_count is None: if video_count is None:
video_count = 40 video_count = 1000
return flask.render_template( return flask.render_template(
'playlist.html', 'playlist.html',

View File

@@ -58,7 +58,7 @@
}, },
}); });
const player = new Plyr(document.getElementById('js-video-player'), { const playerOptions = {
// Learning about autoplay permission https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy/autoplay#syntax // Learning about autoplay permission https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy/autoplay#syntax
autoplay: autoplayActive, autoplay: autoplayActive,
disableContextMenu: false, disableContextMenu: false,
@@ -117,5 +117,20 @@
tooltips: { tooltips: {
controls: true, controls: true,
}, },
}
const player = new Plyr(document.getElementById('js-video-player'), playerOptions);
// disable double click to fullscreen
// https://github.com/sampotts/plyr/issues/1370#issuecomment-528966795
player.eventListeners.forEach(function(eventListener) {
if(eventListener.type === 'dblclick') {
eventListener.element.removeEventListener(eventListener.type, eventListener.callback, eventListener.options);
}
}); });
// Add .started property, true after the playback has been started
// Needed so controls won't be hidden before playback has started
player.started = false;
player.once('playing', function(){this.started = true});
})(); })();

View File

@@ -37,3 +37,41 @@ e.g. Firefox playback speed options */
max-height: 320px; max-height: 320px;
overflow-y: auto; overflow-y: auto;
} }
/*
* Custom styles similar to youtube
*/
.plyr__controls {
display: flex;
justify-content: center;
}
.plyr__progress__container {
position: absolute;
bottom: 0;
width: 100%;
margin-bottom: -10px;
}
.plyr__controls .plyr__controls__item:first-child {
margin-left: 0;
margin-right: 0;
z-index: 5;
}
.plyr__controls .plyr__controls__item.plyr__volume {
margin-left: auto;
}
.plyr__controls .plyr__controls__item.plyr__progress__container {
padding-left: 10px;
padding-right: 10px;
}
.plyr__progress input[type="range"] {
margin-bottom: 50px;
}
/*
* End custom styles
*/

View File

@@ -318,10 +318,11 @@ def fetch_url(url, headers=(), timeout=15, report_text=None, data=None,
cleanup_func(response) # release_connection for urllib3 cleanup_func(response) # release_connection for urllib3
content = decode_content( content = decode_content(
content, content,
response.getheader('Content-Encoding', default='identity')) response.headers.get('Content-Encoding', default='identity'))
if (settings.debugging_save_responses if (settings.debugging_save_responses
and debug_name is not None and content): and debug_name is not None
and content):
save_dir = os.path.join(settings.data_dir, 'debug') save_dir = os.path.join(settings.data_dir, 'debug')
if not os.path.exists(save_dir): if not os.path.exists(save_dir):
os.makedirs(save_dir) os.makedirs(save_dir)
@@ -394,23 +395,22 @@ def head(url, use_tor=False, report_text=None, max_redirects=10):
round(time.monotonic() - start_time, 3)) round(time.monotonic() - start_time, 3))
return response return response
mobile_user_agent = 'Mozilla/5.0 (Linux; Android 7.0; Redmi Note 4 Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36'
mobile_user_agent = 'Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.80 Mobile Safari/537.36'
mobile_ua = (('User-Agent', mobile_user_agent),) mobile_ua = (('User-Agent', mobile_user_agent),)
desktop_user_agent = 'Mozilla/5.0 (Windows NT 10.0; rv:124.0) Gecko/20100101 Firefox/124.0' desktop_user_agent = 'Mozilla/5.0 (Windows NT 6.1; rv:52.0) Gecko/20100101 Firefox/52.0'
desktop_ua = (('User-Agent', desktop_user_agent),) desktop_ua = (('User-Agent', desktop_user_agent),)
json_header = (('Content-Type', 'application/json'),) json_header = (('Content-Type', 'application/json'),)
desktop_xhr_headers = ( desktop_xhr_headers = (
('Accept', '*/*'), ('Accept', '*/*'),
('Accept-Language', 'en-US,en;q=0.5'), ('Accept-Language', 'en-US,en;q=0.5'),
('X-YouTube-Client-Name', '1'), ('X-YouTube-Client-Name', '1'),
('X-YouTube-Client-Version', '2.20240327.00.00'), ('X-YouTube-Client-Version', '2.20240304.00.00'),
) + desktop_ua ) + desktop_ua
mobile_xhr_headers = ( mobile_xhr_headers = (
('Accept', '*/*'), ('Accept', '*/*'),
('Accept-Language', 'en-US,en;q=0.5'), ('Accept-Language', 'en-US,en;q=0.5'),
('X-YouTube-Client-Name', '1'), ('X-YouTube-Client-Name', '2'),
('X-YouTube-Client-Version', '2.20240328.08.00'), ('X-YouTube-Client-Version', '2.20240304.08.00'),
) + mobile_ua ) + mobile_ua
@@ -431,29 +431,34 @@ class RateLimitedQueue(gevent.queue.Queue):
gevent.queue.Queue.__init__(self) gevent.queue.Queue.__init__(self)
def get(self): def get(self):
with self.lock: # blocks if another greenlet currently has the lock self.lock.acquire() # blocks if another greenlet currently has the lock
if ((self.count_since_last_wait >= self.subsequent_bursts and self.surpassed_initial) or if self.count_since_last_wait >= self.subsequent_bursts and self.surpassed_initial:
(self.count_since_last_wait >= self.initial_burst and not self.surpassed_initial)): gevent.sleep(self.waiting_period)
self.surpassed_initial = True self.count_since_last_wait = 0
gevent.sleep(self.waiting_period)
elif self.count_since_last_wait >= self.initial_burst and not self.surpassed_initial:
self.surpassed_initial = True
gevent.sleep(self.waiting_period)
self.count_since_last_wait = 0
self.count_since_last_wait += 1
if not self.currently_empty and self.empty():
self.currently_empty = True
self.empty_start = time.monotonic()
item = gevent.queue.Queue.get(self) # blocks when nothing left
if self.currently_empty:
if time.monotonic() - self.empty_start >= self.waiting_period:
self.count_since_last_wait = 0 self.count_since_last_wait = 0
self.surpassed_initial = False
self.count_since_last_wait += 1 self.currently_empty = False
if not self.currently_empty and self.empty(): self.lock.release()
self.currently_empty = True
self.empty_start = time.monotonic()
item = gevent.queue.Queue.get(self) # blocks when nothing left return item
if self.currently_empty:
if time.monotonic() - self.empty_start >= self.waiting_period:
self.count_since_last_wait = 0
self.surpassed_initial = False
self.currently_empty = False
return item
def download_thumbnail(save_directory, video_id): def download_thumbnail(save_directory, video_id):
@@ -662,6 +667,29 @@ def to_valid_filename(name):
# https://github.com/yt-dlp/yt-dlp/blob/master/yt_dlp/extractor/youtube.py#L72 # https://github.com/yt-dlp/yt-dlp/blob/master/yt_dlp/extractor/youtube.py#L72
INNERTUBE_CLIENTS = { INNERTUBE_CLIENTS = {
'android': {
'INNERTUBE_API_KEY': 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w',
'INNERTUBE_CONTEXT': {
'client': {
'hl': 'en',
'gl': 'US',
'clientName': 'ANDROID',
'clientVersion': '19.09.36',
'osName': 'Android',
'osVersion': '12',
'androidSdkVersion': 31,
'platform': 'MOBILE',
'userAgent': 'com.google.android.youtube/19.09.36 (Linux; U; Android 12; US) gzip'
},
# https://github.com/yt-dlp/yt-dlp/pull/575#issuecomment-887739287
#'thirdParty': {
# 'embedUrl': 'https://google.com', # Can be any valid URL
#}
},
'INNERTUBE_CONTEXT_CLIENT_NAME': 3,
'REQUIRE_JS_PLAYER': False,
},
'android-test-suite': { 'android-test-suite': {
'INNERTUBE_API_KEY': 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w', 'INNERTUBE_API_KEY': 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w',
'INNERTUBE_CONTEXT': { 'INNERTUBE_CONTEXT': {
@@ -692,53 +720,15 @@ INNERTUBE_CLIENTS = {
'hl': 'en', 'hl': 'en',
'gl': 'US', 'gl': 'US',
'clientName': 'IOS', 'clientName': 'IOS',
'clientVersion': '19.12.3', 'clientVersion': '19.09.3',
'deviceModel': 'iPhone14,3', 'deviceModel': 'iPhone14,3',
'userAgent': 'com.google.ios.youtube/19.12.3 (iPhone14,3; U; CPU iOS 15_6 like Mac OS X)' 'userAgent': 'com.google.ios.youtube/19.09.3 (iPhone14,3; U; CPU iOS 15_6 like Mac OS X)'
} }
}, },
'INNERTUBE_CONTEXT_CLIENT_NAME': 5, 'INNERTUBE_CONTEXT_CLIENT_NAME': 5,
'REQUIRE_JS_PLAYER': False 'REQUIRE_JS_PLAYER': False
}, },
'android': {
'INNERTUBE_API_KEY': 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w',
'INNERTUBE_CONTEXT': {
'client': {
'hl': 'en',
'gl': 'US',
'clientName': 'ANDROID',
'clientVersion': '19.15.35',
'osName': 'Android',
'osVersion': '14',
'androidSdkVersion': 34,
'platform': 'MOBILE',
'userAgent': 'com.google.android.youtube/19.15.35 (Linux; U; Android 14; en_US; Google Pixel 6 Pro) gzip'
}
},
'INNERTUBE_CONTEXT_CLIENT_NAME': 3,
'REQUIRE_JS_PLAYER': False,
},
'android_music': {
'INNERTUBE_API_KEY': 'AIzaSyAOghZGza2MQSZkY_zfZ370N-PUdXEo8AI',
'INNERTUBE_CONTEXT': {
'client': {
'hl': 'en',
'gl': 'US',
'clientName': 'ANDROID_MUSIC',
'clientVersion': '6.48.51',
'osName': 'Android',
'osVersion': '14',
'androidSdkVersion': 34,
'platform': 'MOBILE',
'userAgent': 'com.google.android.apps.youtube.music/6.48.51 (Linux; U; Android 14; US) gzip'
}
},
'INNERTUBE_CONTEXT_CLIENT_NAME': 21,
'REQUIRE_JS_PLAYER': False
},
# This client can access age restricted videos (unless the uploader has disabled the 'allow embedding' option) # This client can access age restricted videos (unless the uploader has disabled the 'allow embedding' option)
# See: https://github.com/zerodytrash/YouTube-Internal-Clients # See: https://github.com/zerodytrash/YouTube-Internal-Clients
'tv_embedded': { 'tv_embedded': {
@@ -766,14 +756,62 @@ INNERTUBE_CLIENTS = {
'INNERTUBE_CONTEXT': { 'INNERTUBE_CONTEXT': {
'client': { 'client': {
'clientName': 'WEB', 'clientName': 'WEB',
'clientVersion': '2.20240327.00.00', 'clientVersion': '2.20220801.00.00',
'userAgent': desktop_user_agent, 'userAgent': desktop_user_agent,
} }
}, },
'INNERTUBE_CONTEXT_CLIENT_NAME': 1 'INNERTUBE_CONTEXT_CLIENT_NAME': 1
}, },
'android_vr': {
'INNERTUBE_API_KEY': 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w',
'INNERTUBE_CONTEXT': {
'client': {
'clientName': 'ANDROID_VR',
'clientVersion': '1.60.19',
'deviceMake': 'Oculus',
'deviceModel': 'Quest 3',
'androidSdkVersion': 32,
'userAgent': 'com.google.android.apps.youtube.vr.oculus/1.60.19 (Linux; U; Android 12L; eureka-user Build/SQ3A.220605.009.A1) gzip',
'osName': 'Android',
'osVersion': '12L',
},
},
'INNERTUBE_CONTEXT_CLIENT_NAME': 28,
'REQUIRE_JS_PLAYER': False,
},
} }
def get_visitor_data():
visitor_data = None
visitor_data_cache = os.path.join(settings.data_dir, 'visitorData.txt')
if not os.path.exists(settings.data_dir):
os.makedirs(settings.data_dir)
if os.path.isfile(visitor_data_cache):
with open(visitor_data_cache, 'r') as file:
print('Getting visitor_data from cache')
visitor_data = file.read()
max_age = 12*3600
file_age = time.time() - os.path.getmtime(visitor_data_cache)
if file_age > max_age:
print('visitor_data cache is too old. Removing file...')
os.remove(visitor_data_cache)
return visitor_data
print('Fetching youtube homepage to get visitor_data')
yt_homepage = 'https://www.youtube.com'
yt_resp = fetch_url(yt_homepage, headers={'User-Agent': mobile_user_agent}, report_text='Getting youtube homepage')
visitor_data_re = r'''"visitorData":\s*?"(.+?)"'''
visitor_data_match = re.search(visitor_data_re, yt_resp.decode())
if visitor_data_match:
visitor_data = visitor_data_match.group(1)
print(f'Got visitor_data: {len(visitor_data)}')
with open(visitor_data_cache, 'w') as file:
print('Saving visitor_data cache...')
file.write(visitor_data)
return visitor_data
else:
print('Unable to get visitor_data value')
return visitor_data
def call_youtube_api(client, api, data): def call_youtube_api(client, api, data):
client_params = INNERTUBE_CLIENTS[client] client_params = INNERTUBE_CLIENTS[client]
@@ -781,12 +819,17 @@ def call_youtube_api(client, api, data):
key = client_params['INNERTUBE_API_KEY'] key = client_params['INNERTUBE_API_KEY']
host = client_params.get('INNERTUBE_HOST') or 'www.youtube.com' host = client_params.get('INNERTUBE_HOST') or 'www.youtube.com'
user_agent = context['client'].get('userAgent') or mobile_user_agent user_agent = context['client'].get('userAgent') or mobile_user_agent
visitor_data = get_visitor_data()
url = 'https://' + host + '/youtubei/v1/' + api + '?key=' + key url = 'https://' + host + '/youtubei/v1/' + api + '?key=' + key
if visitor_data:
context['client'].update({'visitorData': visitor_data})
data['context'] = context data['context'] = context
data = json.dumps(data) data = json.dumps(data)
headers = (('Content-Type', 'application/json'),('User-Agent', user_agent)) headers = (('Content-Type', 'application/json'),('User-Agent', user_agent))
if visitor_data:
headers = ( *headers, ('X-Goog-Visitor-Id', visitor_data ))
response = fetch_url( response = fetch_url(
url, data=data, headers=headers, url, data=data, headers=headers,
debug_name='youtubei_' + api + '_' + client, debug_name='youtubei_' + api + '_' + client,

View File

@@ -1,3 +1,3 @@
from __future__ import unicode_literals from __future__ import unicode_literals
__version__ = '0.2.18' __version__ = 'v0.3.0'

View File

@@ -2,7 +2,6 @@ import youtube
from youtube import yt_app from youtube import yt_app
from youtube import util, comments, local_playlist, yt_data_extract from youtube import util, comments, local_playlist, yt_data_extract
from youtube.util import time_utc_isoformat from youtube.util import time_utc_isoformat
from youtube.util import INNERTUBE_CLIENTS
import settings import settings
from flask import request from flask import request
@@ -344,7 +343,6 @@ def _add_to_error(info, key, additional_message):
def fetch_player_response(client, video_id): def fetch_player_response(client, video_id):
return util.call_youtube_api(client, 'player', { return util.call_youtube_api(client, 'player', {
'videoId': video_id, 'videoId': video_id,
'params': 'CgIIAdgDAQ==',
}) })
@@ -370,83 +368,83 @@ def fetch_watch_page_info(video_id, playlist_id, index):
return yt_data_extract.extract_watch_info_from_html(watch_page) return yt_data_extract.extract_watch_info_from_html(watch_page)
def extract_info(video_id, use_invidious, playlist_id=None, index=None): def extract_info(video_id, use_invidious, playlist_id=None, index=None):
for client in INNERTUBE_CLIENTS: tasks = (
tasks = ( # Get video metadata from here
gevent.spawn(fetch_watch_page_info, video_id, playlist_id, index), gevent.spawn(fetch_watch_page_info, video_id, playlist_id, index),
gevent.spawn(fetch_player_response, client, video_id) # Use client from INNERTUBE_CLIENTS gevent.spawn(fetch_player_response, 'android_vr', video_id)
) )
gevent.joinall(tasks) gevent.joinall(tasks)
util.check_gevent_exceptions(*tasks) util.check_gevent_exceptions(*tasks)
info, player_response = tasks[0].value, tasks[1].value info, player_response = tasks[0].value, tasks[1].value
yt_data_extract.update_with_new_urls(info, player_response)
# Age restricted video, retry
if info['age_restricted'] or info['player_urls_missing']:
if info['age_restricted']:
print('Age restricted video, retrying')
else:
print('Player urls missing, retrying')
player_response = fetch_player_response('tv_embedded', video_id)
yt_data_extract.update_with_new_urls(info, player_response) yt_data_extract.update_with_new_urls(info, player_response)
# Age restricted video, retry # signature decryption
if info['age_restricted'] or info['player_urls_missing']: decryption_error = decrypt_signatures(info, video_id)
if info['age_restricted']: if decryption_error:
print('Age restricted video, retrying') decryption_error = 'Error decrypting url signatures: ' + decryption_error
else: info['playability_error'] = decryption_error
print('Player urls missing, retrying')
player_response = fetch_player_response('tv_embedded', video_id)
yt_data_extract.update_with_new_urls(info, player_response)
# signature decryption # check if urls ready (non-live format) in former livestream
decryption_error = decrypt_signatures(info, video_id) # urls not ready if all of them have no filesize
if decryption_error: if info['was_live']:
decryption_error = 'Error decrypting url signatures: ' + decryption_error info['urls_ready'] = False
info['playability_error'] = decryption_error for fmt in info['formats']:
if fmt['file_size'] is not None:
info['urls_ready'] = True
else:
info['urls_ready'] = True
# check if urls ready (non-live format) in former livestream # livestream urls
# urls not ready if all of them have no filesize # sometimes only the livestream urls work soon after the livestream is over
if info['was_live']: if (info['hls_manifest_url']
info['urls_ready'] = False and (info['live'] or not info['formats'] or not info['urls_ready'])
):
manifest = util.fetch_url(info['hls_manifest_url'],
debug_name='hls_manifest.m3u8',
report_text='Fetched hls manifest'
).decode('utf-8')
info['hls_formats'], err = yt_data_extract.extract_hls_formats(manifest)
if not err:
info['playability_error'] = None
for fmt in info['hls_formats']:
fmt['video_quality'] = video_quality_string(fmt)
else:
info['hls_formats'] = []
# check for 403. Unnecessary for tor video routing b/c ip address is same
info['invidious_used'] = False
info['invidious_reload_button'] = False
info['tor_bypass_used'] = False
if (settings.route_tor == 1
and info['formats'] and info['formats'][0]['url']):
try:
response = util.head(info['formats'][0]['url'],
report_text='Checked for URL access')
except urllib3.exceptions.HTTPError:
print('Error while checking for URL access:\n')
traceback.print_exc()
return info
if response.status == 403:
print('Access denied (403) for video urls.')
print('Routing video through Tor')
info['tor_bypass_used'] = True
for fmt in info['formats']: for fmt in info['formats']:
if fmt['file_size'] is not None: fmt['url'] += '&use_tor=1'
info['urls_ready'] = True elif 300 <= response.status < 400:
else: print('Error: exceeded max redirects while checking video URL')
info['urls_ready'] = True return info
# livestream urls
# sometimes only the livestream urls work soon after the livestream is over
if (info['hls_manifest_url']
and (info['live'] or not info['formats'] or not info['urls_ready'])
):
manifest = util.fetch_url(info['hls_manifest_url'],
debug_name='hls_manifest.m3u8',
report_text='Fetched hls manifest'
).decode('utf-8')
info['hls_formats'], err = yt_data_extract.extract_hls_formats(manifest)
if not err:
info['playability_error'] = None
for fmt in info['hls_formats']:
fmt['video_quality'] = video_quality_string(fmt)
else:
info['hls_formats'] = []
# check for 403. Unnecessary for tor video routing b/c ip address is same
info['invidious_used'] = False
info['invidious_reload_button'] = False
info['tor_bypass_used'] = False
if (settings.route_tor == 1
and info['formats'] and info['formats'][0]['url']):
try:
response = util.head(info['formats'][0]['url'],
report_text='Checked for URL access')
except urllib3.exceptions.HTTPError:
print('Error while checking for URL access:\n')
traceback.print_exc()
return info
if response.status == 403:
print('Access denied (403) for video urls.')
print('Routing video through Tor')
info['tor_bypass_used'] = True
for fmt in info['formats']:
fmt['url'] += '&use_tor=1'
elif 300 <= response.status < 400:
print('Error: exceeded max redirects while checking video URL')
return info
def video_quality_string(format): def video_quality_string(format):
@@ -651,12 +649,6 @@ def get_watch_page(video_id=None):
'/videoplayback', '/videoplayback',
'/videoplayback/name/' + filename) '/videoplayback/name/' + filename)
if settings.gather_googlevideo_domains:
with open(os.path.join(settings.data_dir, 'googlevideo-domains.txt'), 'a+', encoding='utf-8') as f:
url = info['formats'][0]['url']
subdomain = url[0:url.find(".googlevideo.com")]
f.write(subdomain + "\n")
download_formats = [] download_formats = []
for format in (info['formats'] + info['hls_formats']): for format in (info['formats'] + info['hls_formats']):