Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature innertube client fix #219

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions generate_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def wine_run(command_parts):
# confused with working directory. I'm calling it the same thing so it will
# have that name when extracted from the final release zip archive)
log('Making copy of youtube-local files')
check(os.system('git archive --format tar master | 7z x -si -ttar -oyoutube-local'))
check(os.system('git archive --format tar HEAD | 7z x -si -ttar -oyoutube-local'))

if len(os.listdir('./youtube-local')) == 0:
raise Exception('Failed to copy youtube-local files')
Expand Down Expand Up @@ -217,10 +217,10 @@ def wine_run(command_parts):
log('Removing pyc files') # Have to do this because get-pip and some packages don't respect --no-compile
remove_files_with_extensions(r'./python', ['.pyc'])

log('Removing dist-info and __pycache__')
log('Removing __pycache__')
for root, dirs, files in os.walk(r'./python'):
for dir in dirs:
if dir == '__pycache__' or dir.endswith('.dist-info'):
if dir == '__pycache__':
shutil.rmtree(os.path.join(root, dir))


Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ urllib3>=1.24.1
defusedxml>=0.5.0
cachetools>=4.0.0
stem>=1.8.0
fake-useragent>=2.0.0
dukpy>=0.4.0
45 changes: 43 additions & 2 deletions server.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
import re
import sys
import time

import os
import json



Expand Down Expand Up @@ -50,11 +51,49 @@ def parse_range(range_header, content_length):
end_byte = int(end)
return start_byte, end_byte

innertube_client_id = settings.innertube_client_id
innertube_client = util.innertube_client
client = innertube_client[innertube_client_id]
def proxy_site(env, start_response, video=False):
client_params = util.INNERTUBE_CLIENTS[client]
client_context = client_params['INNERTUBE_CONTEXT']
client_ua = client_context.get('userAgent') or util.mobile_user_agent
send_headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64)',
'User-Agent': client_ua,
'Accept': '*/*',
'Accept-language': 'en-US',
'X-YouTube-Client-Name': client_params['INNERTUBE_CONTEXT_CLIENT_NAME'],
'X-YouTube-Client-Version': client_context['client']['clientVersion'],
}
visitor_data_file = os.path.join(settings.data_dir,'visitorData.txt')
visitor_data = None
if not settings.use_po_token:
if settings.use_visitor_data:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe change to if not settings.use_po_token and settings.use_visitor_data:? Having trouble understanding the intent of the if's and else's in this tree. If as written is intended, you don't want to nest if statements unnecessarily when you can use an and instead

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It didn't occur in my mind when I was modifying this to use and statement. Thanks for pointing out this better approach.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really mean nested if statement instead of adding settings.use_visitor_data as additional condition for loading visitorData file.

There was a wrong indentation at the next else statement.

if os.path.exists(visitor_data_file):
try:
with open(visitor_data_file, "r") as file:
visitor_data = file.read()
file.close()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need file.close() when using with context - it will automatically close the file

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't know about that behavior before. Thanks for the hint.

except OSError:
print('An OS error prevents accessing visitorData.txt file')
else:
po_token_cache = os.path.join(settings.data_dir, 'po_token_cache.txt')
if os.path.exists(po_token_cache):
try:
with open(po_token_cache, "r") as file:
po_token_dict = json.loads(file.read())
visitor_data = po_token_dict.get('visitorData') or None
file.close()
except OSError:
print('An OS error prevents accessing po_token_cache.txt')

google_domains = [ 'youtube.com', 'youtube-nocookie.com', 'youtu.be', 'googlevideo.com', 'ytimg.com', 'ggpht.com', 'googleapis.com' ]
for domain in google_domains:
if env['SERVER_NAME'].endswith(domain):
if visitor_data != None:
send_headers['X-Goog-Visitor-Id'] = visitor_data
break

current_range_start = 0
range_end = None
if 'HTTP_RANGE' in env:
Expand Down Expand Up @@ -92,6 +131,8 @@ def proxy_site(env, start_response, video=False):
except AttributeError:
pass
if video:
send_headers = (list(send_headers)
+ [('origin', 'https://www.youtube.com')])
response_headers = (list(response_headers)
+[('Access-Control-Allow-Origin', '*')])

Expand Down
60 changes: 60 additions & 0 deletions settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,56 @@
'category': 'interface',
}),

('use_video_download', {
'type': int,
'default': 0,
'comment': '',
'options': [
(0, 'Disabled'),
(1, 'Enabled'),
],
'category': 'interface',
'comment': '''If enabled, you may incur legal issues with RIAA. Disabled by default.
More info: https://torrentfreak.com/riaa-thwarts-youts-attempt-to-declare-youtube-ripping-legal-221002/
Archive: https://archive.ph/OZQbN''',
}),

('innertube_client_id', {
'type': int,
'default': 6,
'label': 'innertube client',
'comment': '''innertube client to access YouTube API, i.e. fetching player data
Available clients: android, android-test-suite, ios, tv_embedded, web, web_creator, mweb
''',
'options': [
(0, 'android'),
(1, 'android-test-suite'),
(2, 'ios'),
(3, 'tv_embedded'),
(4, 'web'),
(5, 'web_creator'),
(6, 'mweb'),
],
'category': 'network',
}),

('use_visitor_data', {
'label': 'Use visitor data',
'type': bool,
'default': False,
'comment': '''Add X-Goog-Visitor-Id header to outgoing http requests''',
'category': 'network',
}),

('use_po_token', {
'label': 'Use po_token',
'type': bool,
'default': False,
'comment': '''Use visitor_data and po_token using po-token-generator''',
'category': 'network',
}),


('proxy_images', {
'label': 'Route images',
'type': bool,
Expand Down Expand Up @@ -309,6 +359,16 @@
'comment': '',
}),

('allow_age_restricted_content', {
'type': bool,
'label': 'Allow playback of age restricted videos',
'default': False,
'comment': '''Allow playback of age restricted videos.
If set to True, the system will try reloading the video player when encountering age restricted content.''',
'category': 'playback',
}),


('gather_googlevideo_domains', {
'type': bool,
'default': False,
Expand Down
3 changes: 2 additions & 1 deletion youtube/channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
('X-YouTube-Client-Name', '2'),
('X-YouTube-Client-Version', '2.20180830'),
) + util.mobile_ua
headers_client = util.client_xhr_headers
real_cookie = (('Cookie', 'VISITOR_INFO1_LIVE=8XihrAcN1l4'),)
generic_cookie = (('Cookie', 'VISITOR_INFO1_LIVE=ST1Ti53r4fU'),)

Expand Down Expand Up @@ -283,7 +284,7 @@ def get_number_of_videos_channel(channel_id):
url = 'https://m.youtube.com/playlist?list=' + playlist_id + '&pbj=1'

try:
response = util.fetch_url(url, headers_mobile,
response = util.fetch_url(url, headers_client,
debug_name='number_of_videos', report_text='Got number of videos')
except urllib.error.HTTPError as e:
traceback.print_exc()
Expand Down
2 changes: 1 addition & 1 deletion youtube/comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def request_comments(ctoken, replies=False):
})

content = util.fetch_url(
url, headers=util.mobile_xhr_headers + util.json_header, data=data,
url, headers=util.client_xhr_headers + util.json_header, data=data,
report_text='Retrieved comments', debug_name='request_comments')
content = content.decode('utf-8')

Expand Down
5 changes: 3 additions & 2 deletions youtube/proto_debug.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# TODO: clean this file up more and heavily refactor
from fake_useragent import UserAgent

''' Helper functions for reverse engineering protobuf.

Expand Down Expand Up @@ -571,15 +572,15 @@ def pp(obj, indent=1):
print(_pp(obj, indent))


desktop_user_agent = 'Mozilla/5.0 (Windows NT 6.1; rv:52.0) Gecko/20100101 Firefox/52.0'
desktop_user_agent = UserAgent(os='Windows').firefox
desktop_headers = (
('Accept', '*/*'),
('Accept-Language', 'en-US,en;q=0.5'),
('X-YouTube-Client-Name', '1'),
('X-YouTube-Client-Version', '2.20180830'),
) + (('User-Agent', desktop_user_agent),)

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 = UserAgent(os='Android').chrome
mobile_headers = (
('Accept', '*/*'),
('Accept-Language', 'en-US,en;q=0.5'),
Expand Down
5 changes: 4 additions & 1 deletion youtube/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,14 @@ def page_number_to_sp_parameter(page, autocorrect, sort, filters):
result = proto.uint(1, sort) + filters_enc + autocorrect + proto.uint(9, offset) + proto.string(61, b'')
return base64.urlsafe_b64encode(result).decode('ascii')

desktop_user_agent = util.desktop_user_agent
mobile_user_agent = util.mobile_user_agent

def get_search_json(query, page, autocorrect, sort, filters):
url = "https://www.youtube.com/results?search_query=" + urllib.parse.quote_plus(query)
headers = {
'Host': 'www.youtube.com',
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64)',
'User-Agent': desktop_user_agent,
'Accept': '*/*',
'Accept-Language': 'en-US,en;q=0.5',
'X-YouTube-Client-Name': '1',
Expand Down
3 changes: 2 additions & 1 deletion youtube/templates/watch.html
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,7 @@ <h2 class="title">{{ title }}</h2>

<span class="direct-link"><a href="https://youtu.be/{{ video_id }}">Direct Link</a></span>

{% if settings.use_video_download != 0 %}
<details class="download-dropdown">
<summary class="download-dropdown-label">Download</summary>
<ul class="download-dropdown-content">
Expand Down Expand Up @@ -556,7 +557,7 @@ <h2 class="title">{{ title }}</h2>
{% endfor %}
</ul>
</details>

{% endif %}

<span class="description">{{ common_elements.text_runs(description)|escape|urlize|timestamps|safe }}</span>
<div class="music-list">
Expand Down
Loading