Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6ca011202 | ||
|
|
114c2572a4 | ||
|
f64b362603
|
|||
|
2fd7910194
|
|||
|
c2e53072f7
|
|||
|
c2986f3b14
|
|||
|
57854169f4
|
|||
|
3217305f9f
|
|||
|
639aadd2c1
|
|||
|
7157df13cd
|
|||
|
630e0137e0
|
|||
|
a0c51731af
|
|||
|
d361996fc0
|
|||
|
|
4ef7dda14a | ||
|
|
ee31cedae0 | ||
|
d3b0cb5e13
|
|||
|
0a79974d11
|
|||
|
4e327944a0
|
|||
|
09a437f7fb
|
|||
|
3cbe18aac0
|
|||
|
|
62418f8e95 |
23
.gitea/workflows/ci.yaml
Normal file
23
.gitea/workflows/ci.yaml
Normal 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
|
||||||
40
.gitea/workflows/git-sync.yaml
Normal file
40
.gitea/workflows/git-sync.yaml
Normal 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
|
||||||
@@ -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.3
|
|
||||||
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.1
|
|
||||||
Werkzeug==3.0.1
|
|
||||||
zope.event==5.0
|
|
||||||
zope.interface==6.2
|
|
||||||
|
|||||||
@@ -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.3
|
|
||||||
MarkupSafe==2.1.5
|
|
||||||
PySocks==1.7.1
|
|
||||||
stem==1.8.2
|
|
||||||
urllib3==2.2.1
|
|
||||||
Werkzeug==3.0.1
|
|
||||||
zope.event==5.0
|
|
||||||
zope.interface==6.2
|
|
||||||
|
|||||||
18
settings.py
18
settings.py
@@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
if call(["git", "branch"], stderr=STDOUT, stdout=open(os.devnull, 'w')) != 0:
|
||||||
stdout=open(os.devnull, 'w')) != 0:
|
return subst_list
|
||||||
subst_list
|
|
||||||
else:
|
describe = minimal_env_cmd(["git", "describe", "--tags", "--always"])
|
||||||
# version
|
|
||||||
describe = minimal_env_cmd(["git", "describe", "--always"])
|
|
||||||
git_revision = describe.strip().decode('ascii')
|
git_revision = describe.strip().decode('ascii')
|
||||||
# branch
|
|
||||||
branch = minimal_env_cmd(["git", "branch"])
|
branch = minimal_env_cmd(["git", "branch"])
|
||||||
git_branch = branch.strip().decode('ascii').replace('* ', '')
|
git_branch = branch.strip().decode('ascii').replace('* ', '')
|
||||||
|
|
||||||
subst_list = {
|
subst_list.update({
|
||||||
"version": __version__,
|
|
||||||
"branch": git_branch,
|
"branch": git_branch,
|
||||||
"commit": git_revision
|
"commit": git_revision
|
||||||
}
|
})
|
||||||
|
|
||||||
return subst_list
|
return subst_list
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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});
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -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
|
||||||
|
*/
|
||||||
|
|||||||
148
youtube/util.py
148
youtube/util.py
@@ -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,9 +431,12 @@ 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.count_since_last_wait = 0
|
||||||
|
|
||||||
|
elif self.count_since_last_wait >= self.initial_burst and not self.surpassed_initial:
|
||||||
self.surpassed_initial = True
|
self.surpassed_initial = True
|
||||||
gevent.sleep(self.waiting_period)
|
gevent.sleep(self.waiting_period)
|
||||||
self.count_since_last_wait = 0
|
self.count_since_last_wait = 0
|
||||||
@@ -453,6 +456,8 @@ class RateLimitedQueue(gevent.queue.Queue):
|
|||||||
|
|
||||||
self.currently_empty = False
|
self.currently_empty = False
|
||||||
|
|
||||||
|
self.lock.release()
|
||||||
|
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
@@ -662,22 +667,6 @@ 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 = {
|
||||||
'ios': {
|
|
||||||
'INNERTUBE_API_KEY': 'AIzaSyB-63vPrdThhKuerbB2N_l7Kwwcxj6yUAc',
|
|
||||||
'INNERTUBE_CONTEXT': {
|
|
||||||
'client': {
|
|
||||||
'hl': 'en',
|
|
||||||
'gl': 'US',
|
|
||||||
'clientName': 'IOS',
|
|
||||||
'clientVersion': '19.12.3',
|
|
||||||
'deviceModel': 'iPhone14,3',
|
|
||||||
'userAgent': 'com.google.ios.youtube/19.12.3 (iPhone14,3; U; CPU iOS 15_6 like Mac OS X)'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 5,
|
|
||||||
'REQUIRE_JS_PLAYER': False
|
|
||||||
},
|
|
||||||
|
|
||||||
'android': {
|
'android': {
|
||||||
'INNERTUBE_API_KEY': 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w',
|
'INNERTUBE_API_KEY': 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w',
|
||||||
'INNERTUBE_CONTEXT': {
|
'INNERTUBE_CONTEXT': {
|
||||||
@@ -685,34 +674,58 @@ INNERTUBE_CLIENTS = {
|
|||||||
'hl': 'en',
|
'hl': 'en',
|
||||||
'gl': 'US',
|
'gl': 'US',
|
||||||
'clientName': 'ANDROID',
|
'clientName': 'ANDROID',
|
||||||
'clientVersion': '19.15.35',
|
'clientVersion': '19.09.36',
|
||||||
'osName': 'Android',
|
'osName': 'Android',
|
||||||
'osVersion': '14',
|
'osVersion': '12',
|
||||||
'androidSdkVersion': 34,
|
'androidSdkVersion': 31,
|
||||||
'platform': 'MOBILE',
|
'platform': 'MOBILE',
|
||||||
'userAgent': 'com.google.android.youtube/19.15.35 (Linux; U; Android 14; en_US; Google Pixel 6 Pro) gzip'
|
'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,
|
'INNERTUBE_CONTEXT_CLIENT_NAME': 3,
|
||||||
'REQUIRE_JS_PLAYER': False,
|
'REQUIRE_JS_PLAYER': False,
|
||||||
},
|
},
|
||||||
|
|
||||||
'android_music': {
|
'android-test-suite': {
|
||||||
'INNERTUBE_API_KEY': 'AIzaSyAOghZGza2MQSZkY_zfZ370N-PUdXEo8AI',
|
'INNERTUBE_API_KEY': 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w',
|
||||||
'INNERTUBE_CONTEXT': {
|
'INNERTUBE_CONTEXT': {
|
||||||
'client': {
|
'client': {
|
||||||
'hl': 'en',
|
'hl': 'en',
|
||||||
'gl': 'US',
|
'gl': 'US',
|
||||||
'clientName': 'ANDROID_MUSIC',
|
'clientName': 'ANDROID_TESTSUITE',
|
||||||
'clientVersion': '6.48.51',
|
'clientVersion': '1.9',
|
||||||
'osName': 'Android',
|
'osName': 'Android',
|
||||||
'osVersion': '14',
|
'osVersion': '12',
|
||||||
'androidSdkVersion': 34,
|
'androidSdkVersion': 31,
|
||||||
'platform': 'MOBILE',
|
'platform': 'MOBILE',
|
||||||
'userAgent': 'com.google.android.apps.youtube.music/6.48.51 (Linux; U; Android 14; US) gzip'
|
'userAgent': 'com.google.android.youtube/1.9 (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,
|
||||||
|
},
|
||||||
|
|
||||||
|
'ios': {
|
||||||
|
'INNERTUBE_API_KEY': 'AIzaSyB-63vPrdThhKuerbB2N_l7Kwwcxj6yUAc',
|
||||||
|
'INNERTUBE_CONTEXT': {
|
||||||
|
'client': {
|
||||||
|
'hl': 'en',
|
||||||
|
'gl': 'US',
|
||||||
|
'clientName': 'IOS',
|
||||||
|
'clientVersion': '19.09.3',
|
||||||
|
'deviceModel': 'iPhone14,3',
|
||||||
|
'userAgent': 'com.google.ios.youtube/19.09.3 (iPhone14,3; U; CPU iOS 15_6 like Mac OS X)'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 21,
|
'INNERTUBE_CONTEXT_CLIENT_NAME': 5,
|
||||||
'REQUIRE_JS_PLAYER': False
|
'REQUIRE_JS_PLAYER': False
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -743,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]
|
||||||
@@ -758,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,
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
__version__ = '0.2.17'
|
__version__ = 'v0.3.0'
|
||||||
|
|||||||
@@ -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,10 +368,10 @@ 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)
|
||||||
@@ -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']):
|
||||||
|
|||||||
Reference in New Issue
Block a user