From 2db58930a6f8c955c4d437657bd07e2939a705f2 Mon Sep 17 00:00:00 2001 From: James Taylor Date: Sun, 16 Jun 2019 16:16:03 -0700 Subject: Convert watch page to flask framework --- youtube/__init__.py | 2 + youtube/comments.css | 129 ------------ youtube/favicon.ico | Bin 5694 -> 0 bytes youtube/shared.css | 372 ----------------------------------- youtube/static/comments.css | 129 ++++++++++++ youtube/static/favicon.ico | Bin 0 -> 5694 bytes youtube/static/shared.css | 372 +++++++++++++++++++++++++++++++++++ youtube/templates/error.html | 17 ++ youtube/templates/watch.html | 222 +++++++++++++++++++++ youtube/watch.py | 458 ++++++++++++++++--------------------------- youtube/youtube.py | 105 ---------- 11 files changed, 909 insertions(+), 897 deletions(-) create mode 100644 youtube/__init__.py delete mode 100644 youtube/comments.css delete mode 100644 youtube/favicon.ico delete mode 100644 youtube/shared.css create mode 100644 youtube/static/comments.css create mode 100644 youtube/static/favicon.ico create mode 100644 youtube/static/shared.css create mode 100644 youtube/templates/error.html create mode 100644 youtube/templates/watch.html delete mode 100644 youtube/youtube.py (limited to 'youtube') diff --git a/youtube/__init__.py b/youtube/__init__.py new file mode 100644 index 0000000..0df56d1 --- /dev/null +++ b/youtube/__init__.py @@ -0,0 +1,2 @@ +import flask +yt_app = flask.Flask(__name__) \ No newline at end of file diff --git a/youtube/comments.css b/youtube/comments.css deleted file mode 100644 index 4cec3e1..0000000 --- a/youtube/comments.css +++ /dev/null @@ -1,129 +0,0 @@ -.video-metadata{ - display: grid; - grid-template-columns: auto 1fr; - grid-template-rows: auto auto 1fr auto; -} - .video-metadata > .video-metadata-thumbnail-box{ - grid-row: 1 / span 3; - } - .video-metadata > .title{ - word-wrap:break-word; - grid-row: 1; - } - .video-metadata > h2{ - grid-row: 2; - font-size: 15px; - } - .video-metadata > span{ - grid-row:3; - } - .video-metadata > hr{ - grid-row: 4; - grid-column: 1 / span 2; - width: 100%; - } - -.comment-form{ - display: grid; - align-content: start; - justify-items: start; - align-items: start; -} - #comment-account-options{ - display:grid; - grid-auto-flow: column; - grid-column-gap: 10px; - margin-top:10px; - margin-bottom:10px; - } - #comment-account-options a{ - margin-left:10px; - } - -.comments-area{ - display:grid; -} - .comments-area textarea{ - resize: vertical; - justify-self:stretch; - } - .post-comment-button{ - margin-top:10px; - justify-self:end; - } - .comment-links{ - display:grid; - grid-auto-flow: column; - grid-column-gap: 10px; - justify-content:start; - } - -.comments{ - margin-top:10px; - grid-row-gap: 10px; - display: grid; - align-content:start; -} - -.comment{ - display:grid; - grid-template-columns: auto auto 100px 1fr; - grid-template-rows: 0fr 0fr 0fr 0fr; - background-color: #dadada; - justify-content: start; -} - -.comment .author-avatar{ - grid-column: 1; - grid-row: 1 / span 3; - align-self: start; - margin-right: 5px; - height:32px; - width:32px; -} - -.comment address{ - grid-column: 2; - grid-row: 1; - margin-right:15px; - white-space: nowrap; - overflow:hidden; -} - -.comment .text{ - grid-column: 2 / span 3; - grid-row: 2; - white-space: pre-wrap; - min-width: 0; - word-wrap: break-word; -} - -.comment .permalink{ - grid-column: 3; - grid-row: 1; - white-space: nowrap; - color: black; - -} - - -.comment .likes{ - grid-column:2; - grid-row:3; - font-weight:bold; - white-space: nowrap; -} - -.comment .bottom-row{ - grid-column:2 / span 3; - grid-row:4; - justify-self:start; - display: grid; - grid-auto-flow: column; - grid-column-gap: 10px; -} - -.more-comments{ - justify-self:center; - margin-top:10px; -} diff --git a/youtube/favicon.ico b/youtube/favicon.ico deleted file mode 100644 index 9d6417c..0000000 Binary files a/youtube/favicon.ico and /dev/null differ diff --git a/youtube/shared.css b/youtube/shared.css deleted file mode 100644 index 1b25d7f..0000000 --- a/youtube/shared.css +++ /dev/null @@ -1,372 +0,0 @@ -h1, h2, h3, h4, h5, h6, div, button{ - margin:0; - padding:0; - -} - - -body{ - margin:0; - padding: 0; - color:#222; - - - background-color:#cccccc; - - min-height:100vh; - - display:grid; - grid-template-rows: 50px 1fr; -} - - header{ - background-color:#333333; - - grid-row: 1; - - display:grid; - grid-template-columns: minmax(0px, 3fr) 640px 40px 500px minmax(0px,2fr); - } - - main{ - grid-row: 2; - } - -address{ - font-style:normal; -} - - - - #site-search{ - grid-column: 2; - display: grid; - grid-template-columns: 1fr auto auto; - - } - - #site-search .search-box{ - align-self:center; - height:25px; - border:0; - - grid-column: 1; - } - #site-search .search-button{ - grid-column: 2; - align-self:center; - height:25px; - - border-style:solid; - border-width:1px; - } - #site-search .dropdown{ - margin-left:5px; - grid-column: 3; - align-self:center; - height:25px; - } - #site-search .dropdown button{ - align-self:center; - height:25px; - - border-style:solid; - border-width:1px; - } - #site-search .css-sucks{ - width:0px; - height:0px; - } - #site-search .dropdown-content{ - grid-template-columns: auto auto; - white-space: nowrap; - } - #site-search .dropdown-content h3{ - grid-column:1 / span 2; - } - -.dropdown{ - z-index:1; -} - .dropdown-content{ - display:none; - background-color: #e9e9e9; - } - .dropdown:hover .dropdown-content{ - /* For some reason, if this is just grid, it will insist on being 0px wide just like its 0px by 0px parent */ - /* making it inline-grid happened to fix it */ - display:inline-grid; - } - - - -#header-right{ - grid-column:4; - - display:grid; - grid-template-columns:auto auto auto 1fr; - grid-template-rows: 1fr; - width: 540px; -} - #playlist-edit{ - display:contents; - } - #local-playlists{ - grid-column: 1; - grid-row:1; - align-self: center; - margin-right:5px; - color: #ffffff; - } - #playlist-name-selection{ - grid-column:2; - grid-row:1; - justify-self:start; - align-self: center; - } - #playlist-add-button{ - grid-column:3; - grid-row:1; - align-self: center; - padding-left: 10px; - padding-right: 10px; - } - #item-selection-reset{ - grid-column:4; - grid-row:1; - align-self: center; - justify-self:start; - padding-left: 10px; - padding-right: 10px; - } - - - -.item-list{ - display: grid; - grid-auto-rows: 138px; - grid-row-gap: 10px; - -} - .item-list .video-thumbnail-box{ - width:246px; - } - .item-list .playlist-thumbnail-box{ - width:246px; - } - - -.item-grid{ - display:grid; - grid-template-columns: repeat(auto-fill, 400px); - grid-auto-rows: 94px; - grid-row-gap: 10px; -} - .item-grid .video-thumbnail-box{ - width:168px; - } - .item-grid .playlist-thumbnail-box{ - width:168px; - } - - - -.medium-item-box{ - - display:grid; - grid-template-columns: 1fr 30px; -} -.medium-item{ - background-color:#bcbcbc; - text-decoration:none; - font-size: 12px; - color: #767676; - - display: grid; - align-content: start; - grid-template-columns: auto 1fr auto; - grid-template-rows: auto auto auto auto auto 1fr; -} - .medium-item .title{ - grid-column:2 / span 2; - grid-row:1; - justify-self:start; - min-width: 0; - max-height:3.6em; - overflow:hidden; - - color: #333; - font-size: 16px; - font-weight: 500; - text-decoration:initial; - } - .medium-item address{ - display:inline; - } - /*.medium-item .views{ - grid-column: 3; - grid-row: 2; - justify-self:end; - } - .medium-item time{ - grid-column: 2; - grid-row: 3; - justify-self:start; - }*/ - .medium-item .stats{ - grid-column: 2 / span 2; - grid-row: 2; - max-height:2.4em; - overflow:hidden; - } - - .medium-item .description{ - grid-column: 2 / span 2; - grid-row: 4; - } - .medium-item .badges{ - grid-column: 2 / span 2; - grid-row: 5; - } - /* thumbnail size */ - .medium-item img{ - /*height:138px; - width:246px;*/ - height:100%; - justify-self:center; - } - -.small-item-box{ - color: #767676; - font-size: 12px; - - display:grid; - grid-template-columns: 1fr 30px; - grid-template-rows: 94px; -} - -.small-item{ - background-color:#bcbcbc; - align-content: start; - text-decoration:none; - - display: grid; - grid-template-columns: 168px 1fr; - grid-column-gap: 5px; - grid-template-rows: auto auto auto 1fr; -} - .small-item .title{ - grid-column:2; - grid-row:1; - margin:0; - - color: #333; - font-size: 16px; - font-weight: 500; - text-decoration:initial; - min-width: 0; - justify-self:start; - - overflow:hidden; - max-height: 3.3em; - line-height: 1.1em; - } - .small-item address{ - grid-column: 2; - grid-row: 2; - justify-self: start; - } - - .small-item .views{ - grid-column: 2; - grid-row: 3; - justify-self:start; - } - /* thumbnail size */ - .small-item img{ - /*height:94px; - width:168px;*/ - height:100%; - justify-self:center; - } - -.item-checkbox{ - justify-self:start; - align-self:center; - height:30px; - width:30px; - - grid-column: 2; -} - -/* ---Thumbnails for videos---- */ -.video-thumbnail-box{ - max-height:100%; - - grid-column:1; - grid-row:1 / span 6; - - display:grid; - grid-template-columns: 1fr 0fr; -} - .video-thumbnail-img{ - grid-column:1 / span 2; - grid-row:1; - } - .video-duration{ - grid-column: 2; - grid-row: 1; - align-self: end; - opacity: .8; - color: #ffffff; - font-size: 12px; - background-color: #000000; - } - -/* ---Thumbnails for playlists---- */ -.playlist-thumbnail-box{ - max-height:100%; - - grid-column:1; - grid-row:1 / span 6; - - display:grid; - grid-template-columns: 3fr 2fr; -} - .playlist-thumbnail-img{ - grid-column:1 / span 2; - grid-row:1; - } - .playlist-thumbnail-info{ - grid-column:2; - grid-row:1; - - display: grid; - align-items:center; - - text-align:center; - white-space: pre-line; - opacity: .8; - color: #cfcfcf; - background-color: #000000; - } - -.page-button-row{ - justify-self:center; - display: grid; - grid-auto-columns: 40px; - grid-auto-flow: column; - height: 40px; -} - .page-button{ - background-color: #e9e9e9; - border-style: outset; - border-width: 2px; - font-weight: bold; - text-align: center; - } -.sort-button{ - background-color: #d0d0d0; - padding: 2px; - justify-self: start; -} diff --git a/youtube/static/comments.css b/youtube/static/comments.css new file mode 100644 index 0000000..4cec3e1 --- /dev/null +++ b/youtube/static/comments.css @@ -0,0 +1,129 @@ +.video-metadata{ + display: grid; + grid-template-columns: auto 1fr; + grid-template-rows: auto auto 1fr auto; +} + .video-metadata > .video-metadata-thumbnail-box{ + grid-row: 1 / span 3; + } + .video-metadata > .title{ + word-wrap:break-word; + grid-row: 1; + } + .video-metadata > h2{ + grid-row: 2; + font-size: 15px; + } + .video-metadata > span{ + grid-row:3; + } + .video-metadata > hr{ + grid-row: 4; + grid-column: 1 / span 2; + width: 100%; + } + +.comment-form{ + display: grid; + align-content: start; + justify-items: start; + align-items: start; +} + #comment-account-options{ + display:grid; + grid-auto-flow: column; + grid-column-gap: 10px; + margin-top:10px; + margin-bottom:10px; + } + #comment-account-options a{ + margin-left:10px; + } + +.comments-area{ + display:grid; +} + .comments-area textarea{ + resize: vertical; + justify-self:stretch; + } + .post-comment-button{ + margin-top:10px; + justify-self:end; + } + .comment-links{ + display:grid; + grid-auto-flow: column; + grid-column-gap: 10px; + justify-content:start; + } + +.comments{ + margin-top:10px; + grid-row-gap: 10px; + display: grid; + align-content:start; +} + +.comment{ + display:grid; + grid-template-columns: auto auto 100px 1fr; + grid-template-rows: 0fr 0fr 0fr 0fr; + background-color: #dadada; + justify-content: start; +} + +.comment .author-avatar{ + grid-column: 1; + grid-row: 1 / span 3; + align-self: start; + margin-right: 5px; + height:32px; + width:32px; +} + +.comment address{ + grid-column: 2; + grid-row: 1; + margin-right:15px; + white-space: nowrap; + overflow:hidden; +} + +.comment .text{ + grid-column: 2 / span 3; + grid-row: 2; + white-space: pre-wrap; + min-width: 0; + word-wrap: break-word; +} + +.comment .permalink{ + grid-column: 3; + grid-row: 1; + white-space: nowrap; + color: black; + +} + + +.comment .likes{ + grid-column:2; + grid-row:3; + font-weight:bold; + white-space: nowrap; +} + +.comment .bottom-row{ + grid-column:2 / span 3; + grid-row:4; + justify-self:start; + display: grid; + grid-auto-flow: column; + grid-column-gap: 10px; +} + +.more-comments{ + justify-self:center; + margin-top:10px; +} diff --git a/youtube/static/favicon.ico b/youtube/static/favicon.ico new file mode 100644 index 0000000..9d6417c Binary files /dev/null and b/youtube/static/favicon.ico differ diff --git a/youtube/static/shared.css b/youtube/static/shared.css new file mode 100644 index 0000000..1b25d7f --- /dev/null +++ b/youtube/static/shared.css @@ -0,0 +1,372 @@ +h1, h2, h3, h4, h5, h6, div, button{ + margin:0; + padding:0; + +} + + +body{ + margin:0; + padding: 0; + color:#222; + + + background-color:#cccccc; + + min-height:100vh; + + display:grid; + grid-template-rows: 50px 1fr; +} + + header{ + background-color:#333333; + + grid-row: 1; + + display:grid; + grid-template-columns: minmax(0px, 3fr) 640px 40px 500px minmax(0px,2fr); + } + + main{ + grid-row: 2; + } + +address{ + font-style:normal; +} + + + + #site-search{ + grid-column: 2; + display: grid; + grid-template-columns: 1fr auto auto; + + } + + #site-search .search-box{ + align-self:center; + height:25px; + border:0; + + grid-column: 1; + } + #site-search .search-button{ + grid-column: 2; + align-self:center; + height:25px; + + border-style:solid; + border-width:1px; + } + #site-search .dropdown{ + margin-left:5px; + grid-column: 3; + align-self:center; + height:25px; + } + #site-search .dropdown button{ + align-self:center; + height:25px; + + border-style:solid; + border-width:1px; + } + #site-search .css-sucks{ + width:0px; + height:0px; + } + #site-search .dropdown-content{ + grid-template-columns: auto auto; + white-space: nowrap; + } + #site-search .dropdown-content h3{ + grid-column:1 / span 2; + } + +.dropdown{ + z-index:1; +} + .dropdown-content{ + display:none; + background-color: #e9e9e9; + } + .dropdown:hover .dropdown-content{ + /* For some reason, if this is just grid, it will insist on being 0px wide just like its 0px by 0px parent */ + /* making it inline-grid happened to fix it */ + display:inline-grid; + } + + + +#header-right{ + grid-column:4; + + display:grid; + grid-template-columns:auto auto auto 1fr; + grid-template-rows: 1fr; + width: 540px; +} + #playlist-edit{ + display:contents; + } + #local-playlists{ + grid-column: 1; + grid-row:1; + align-self: center; + margin-right:5px; + color: #ffffff; + } + #playlist-name-selection{ + grid-column:2; + grid-row:1; + justify-self:start; + align-self: center; + } + #playlist-add-button{ + grid-column:3; + grid-row:1; + align-self: center; + padding-left: 10px; + padding-right: 10px; + } + #item-selection-reset{ + grid-column:4; + grid-row:1; + align-self: center; + justify-self:start; + padding-left: 10px; + padding-right: 10px; + } + + + +.item-list{ + display: grid; + grid-auto-rows: 138px; + grid-row-gap: 10px; + +} + .item-list .video-thumbnail-box{ + width:246px; + } + .item-list .playlist-thumbnail-box{ + width:246px; + } + + +.item-grid{ + display:grid; + grid-template-columns: repeat(auto-fill, 400px); + grid-auto-rows: 94px; + grid-row-gap: 10px; +} + .item-grid .video-thumbnail-box{ + width:168px; + } + .item-grid .playlist-thumbnail-box{ + width:168px; + } + + + +.medium-item-box{ + + display:grid; + grid-template-columns: 1fr 30px; +} +.medium-item{ + background-color:#bcbcbc; + text-decoration:none; + font-size: 12px; + color: #767676; + + display: grid; + align-content: start; + grid-template-columns: auto 1fr auto; + grid-template-rows: auto auto auto auto auto 1fr; +} + .medium-item .title{ + grid-column:2 / span 2; + grid-row:1; + justify-self:start; + min-width: 0; + max-height:3.6em; + overflow:hidden; + + color: #333; + font-size: 16px; + font-weight: 500; + text-decoration:initial; + } + .medium-item address{ + display:inline; + } + /*.medium-item .views{ + grid-column: 3; + grid-row: 2; + justify-self:end; + } + .medium-item time{ + grid-column: 2; + grid-row: 3; + justify-self:start; + }*/ + .medium-item .stats{ + grid-column: 2 / span 2; + grid-row: 2; + max-height:2.4em; + overflow:hidden; + } + + .medium-item .description{ + grid-column: 2 / span 2; + grid-row: 4; + } + .medium-item .badges{ + grid-column: 2 / span 2; + grid-row: 5; + } + /* thumbnail size */ + .medium-item img{ + /*height:138px; + width:246px;*/ + height:100%; + justify-self:center; + } + +.small-item-box{ + color: #767676; + font-size: 12px; + + display:grid; + grid-template-columns: 1fr 30px; + grid-template-rows: 94px; +} + +.small-item{ + background-color:#bcbcbc; + align-content: start; + text-decoration:none; + + display: grid; + grid-template-columns: 168px 1fr; + grid-column-gap: 5px; + grid-template-rows: auto auto auto 1fr; +} + .small-item .title{ + grid-column:2; + grid-row:1; + margin:0; + + color: #333; + font-size: 16px; + font-weight: 500; + text-decoration:initial; + min-width: 0; + justify-self:start; + + overflow:hidden; + max-height: 3.3em; + line-height: 1.1em; + } + .small-item address{ + grid-column: 2; + grid-row: 2; + justify-self: start; + } + + .small-item .views{ + grid-column: 2; + grid-row: 3; + justify-self:start; + } + /* thumbnail size */ + .small-item img{ + /*height:94px; + width:168px;*/ + height:100%; + justify-self:center; + } + +.item-checkbox{ + justify-self:start; + align-self:center; + height:30px; + width:30px; + + grid-column: 2; +} + +/* ---Thumbnails for videos---- */ +.video-thumbnail-box{ + max-height:100%; + + grid-column:1; + grid-row:1 / span 6; + + display:grid; + grid-template-columns: 1fr 0fr; +} + .video-thumbnail-img{ + grid-column:1 / span 2; + grid-row:1; + } + .video-duration{ + grid-column: 2; + grid-row: 1; + align-self: end; + opacity: .8; + color: #ffffff; + font-size: 12px; + background-color: #000000; + } + +/* ---Thumbnails for playlists---- */ +.playlist-thumbnail-box{ + max-height:100%; + + grid-column:1; + grid-row:1 / span 6; + + display:grid; + grid-template-columns: 3fr 2fr; +} + .playlist-thumbnail-img{ + grid-column:1 / span 2; + grid-row:1; + } + .playlist-thumbnail-info{ + grid-column:2; + grid-row:1; + + display: grid; + align-items:center; + + text-align:center; + white-space: pre-line; + opacity: .8; + color: #cfcfcf; + background-color: #000000; + } + +.page-button-row{ + justify-self:center; + display: grid; + grid-auto-columns: 40px; + grid-auto-flow: column; + height: 40px; +} + .page-button{ + background-color: #e9e9e9; + border-style: outset; + border-width: 2px; + font-weight: bold; + text-align: center; + } +.sort-button{ + background-color: #d0d0d0; + padding: 2px; + justify-self: start; +} diff --git a/youtube/templates/error.html b/youtube/templates/error.html new file mode 100644 index 0000000..f253807 --- /dev/null +++ b/youtube/templates/error.html @@ -0,0 +1,17 @@ + + + + + Error + + + + + + +{{ header|safe }} +
+{{ error_message }} +
+ + diff --git a/youtube/templates/watch.html b/youtube/templates/watch.html new file mode 100644 index 0000000..7e83306 --- /dev/null +++ b/youtube/templates/watch.html @@ -0,0 +1,222 @@ + + + + + {{ title }} + + + + + + + +{{ header|safe }} +
+
+
+
+ + + +

{{ title }}

+{% if unlisted %} + Unlisted +{% endif %} +
Uploaded by {{ uploader }}
+ {{ views }} views + + + + +
+ +
+{% for format in download_formats %} + + {{ format['ext'] }} + {{ format['resolution'] }} + {{ format['note'] }} + +{% endfor %} +
+
+ + + {{ description }} +
+{{ music_list|safe }} +
+{{ comments|safe }} +
+ + + + + + +
+ + + + + + + diff --git a/youtube/watch.py b/youtube/watch.py index 06b525a..48fd7e3 100644 --- a/youtube/watch.py +++ b/youtube/watch.py @@ -1,127 +1,17 @@ +from youtube import yt_app from youtube import util, html_common, comments +import settings + +from flask import request +import flask from youtube_dl.YoutubeDL import YoutubeDL from youtube_dl.extractor.youtube import YoutubeError import json -import urllib -from string import Template import html - import gevent -import settings import os -video_height_priority = (360, 480, 240, 720, 1080) - - -_formats = { - '5': {'ext': 'flv', 'width': 400, 'height': 240, 'acodec': 'mp3', 'abr': 64, 'vcodec': 'h263'}, - '6': {'ext': 'flv', 'width': 450, 'height': 270, 'acodec': 'mp3', 'abr': 64, 'vcodec': 'h263'}, - '13': {'ext': '3gp', 'acodec': 'aac', 'vcodec': 'mp4v'}, - '17': {'ext': '3gp', 'width': 176, 'height': 144, 'acodec': 'aac', 'abr': 24, 'vcodec': 'mp4v'}, - '18': {'ext': 'mp4', 'width': 640, 'height': 360, 'acodec': 'aac', 'abr': 96, 'vcodec': 'h264'}, - '22': {'ext': 'mp4', 'width': 1280, 'height': 720, 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264'}, - '34': {'ext': 'flv', 'width': 640, 'height': 360, 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'}, - '35': {'ext': 'flv', 'width': 854, 'height': 480, 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'}, - # itag 36 videos are either 320x180 (BaW_jenozKc) or 320x240 (__2ABJjxzNo), abr varies as well - '36': {'ext': '3gp', 'width': 320, 'acodec': 'aac', 'vcodec': 'mp4v'}, - '37': {'ext': 'mp4', 'width': 1920, 'height': 1080, 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264'}, - '38': {'ext': 'mp4', 'width': 4096, 'height': 3072, 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264'}, - '43': {'ext': 'webm', 'width': 640, 'height': 360, 'acodec': 'vorbis', 'abr': 128, 'vcodec': 'vp8'}, - '44': {'ext': 'webm', 'width': 854, 'height': 480, 'acodec': 'vorbis', 'abr': 128, 'vcodec': 'vp8'}, - '45': {'ext': 'webm', 'width': 1280, 'height': 720, 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8'}, - '46': {'ext': 'webm', 'width': 1920, 'height': 1080, 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8'}, - '59': {'ext': 'mp4', 'width': 854, 'height': 480, 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'}, - '78': {'ext': 'mp4', 'width': 854, 'height': 480, 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'}, - - - # 3D videos - '82': {'ext': 'mp4', 'height': 360, 'format_note': '3D', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -20}, - '83': {'ext': 'mp4', 'height': 480, 'format_note': '3D', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -20}, - '84': {'ext': 'mp4', 'height': 720, 'format_note': '3D', 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264', 'preference': -20}, - '85': {'ext': 'mp4', 'height': 1080, 'format_note': '3D', 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264', 'preference': -20}, - '100': {'ext': 'webm', 'height': 360, 'format_note': '3D', 'acodec': 'vorbis', 'abr': 128, 'vcodec': 'vp8', 'preference': -20}, - '101': {'ext': 'webm', 'height': 480, 'format_note': '3D', 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8', 'preference': -20}, - '102': {'ext': 'webm', 'height': 720, 'format_note': '3D', 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8', 'preference': -20}, - - # Apple HTTP Live Streaming - '91': {'ext': 'mp4', 'height': 144, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 48, 'vcodec': 'h264', 'preference': -10}, - '92': {'ext': 'mp4', 'height': 240, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 48, 'vcodec': 'h264', 'preference': -10}, - '93': {'ext': 'mp4', 'height': 360, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -10}, - '94': {'ext': 'mp4', 'height': 480, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -10}, - '95': {'ext': 'mp4', 'height': 720, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 256, 'vcodec': 'h264', 'preference': -10}, - '96': {'ext': 'mp4', 'height': 1080, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 256, 'vcodec': 'h264', 'preference': -10}, - '132': {'ext': 'mp4', 'height': 240, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 48, 'vcodec': 'h264', 'preference': -10}, - '151': {'ext': 'mp4', 'height': 72, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 24, 'vcodec': 'h264', 'preference': -10}, - - # DASH mp4 video - '133': {'ext': 'mp4', 'height': 240, 'format_note': 'DASH video', 'vcodec': 'h264'}, - '134': {'ext': 'mp4', 'height': 360, 'format_note': 'DASH video', 'vcodec': 'h264'}, - '135': {'ext': 'mp4', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'h264'}, - '136': {'ext': 'mp4', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'h264'}, - '137': {'ext': 'mp4', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'h264'}, - '138': {'ext': 'mp4', 'format_note': 'DASH video', 'vcodec': 'h264'}, # Height can vary (https://github.com/rg3/youtube-dl/issues/4559) - '160': {'ext': 'mp4', 'height': 144, 'format_note': 'DASH video', 'vcodec': 'h264'}, - '212': {'ext': 'mp4', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'h264'}, - '264': {'ext': 'mp4', 'height': 1440, 'format_note': 'DASH video', 'vcodec': 'h264'}, - '298': {'ext': 'mp4', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'h264', 'fps': 60}, - '299': {'ext': 'mp4', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'h264', 'fps': 60}, - '266': {'ext': 'mp4', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'h264'}, - - # Dash mp4 audio - '139': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'abr': 48, 'container': 'm4a_dash'}, - '140': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'abr': 128, 'container': 'm4a_dash'}, - '141': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'abr': 256, 'container': 'm4a_dash'}, - '256': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'container': 'm4a_dash'}, - '258': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'container': 'm4a_dash'}, - '325': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'dtse', 'container': 'm4a_dash'}, - '328': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'ec-3', 'container': 'm4a_dash'}, - - # Dash webm - '167': {'ext': 'webm', 'height': 360, 'width': 640, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'}, - '168': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'}, - '169': {'ext': 'webm', 'height': 720, 'width': 1280, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'}, - '170': {'ext': 'webm', 'height': 1080, 'width': 1920, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'}, - '218': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'}, - '219': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'}, - '278': {'ext': 'webm', 'height': 144, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp9'}, - '242': {'ext': 'webm', 'height': 240, 'format_note': 'DASH video', 'vcodec': 'vp9'}, - '243': {'ext': 'webm', 'height': 360, 'format_note': 'DASH video', 'vcodec': 'vp9'}, - '244': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'vp9'}, - '245': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'vp9'}, - '246': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'vp9'}, - '247': {'ext': 'webm', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'vp9'}, - '248': {'ext': 'webm', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'vp9'}, - '271': {'ext': 'webm', 'height': 1440, 'format_note': 'DASH video', 'vcodec': 'vp9'}, - # itag 272 videos are either 3840x2160 (e.g. RtoitU2A-3E) or 7680x4320 (sLprVF6d7Ug) - '272': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'vp9'}, - '302': {'ext': 'webm', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60}, - '303': {'ext': 'webm', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60}, - '308': {'ext': 'webm', 'height': 1440, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60}, - '313': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'vp9'}, - '315': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60}, - - # Dash webm audio - '171': {'ext': 'webm', 'acodec': 'vorbis', 'format_note': 'DASH audio', 'abr': 128}, - '172': {'ext': 'webm', 'acodec': 'vorbis', 'format_note': 'DASH audio', 'abr': 256}, - - # Dash webm audio with opus inside - '249': {'ext': 'webm', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 50}, - '250': {'ext': 'webm', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 70}, - '251': {'ext': 'webm', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 160}, - - # RTMP (unnamed) - '_rtmp': {'protocol': 'rtmp'}, -} - - - - - - -with open("yt_watch_template.html", "r") as file: - yt_watch_template = Template(file.read()) - def get_related_items_html(info): result = "" @@ -156,51 +46,45 @@ def watch_page_related_playlist_info(item): 'thumbnail': util.get_thumbnail_url(item['video_id']), } - -def sort_formats(info): - sorted_formats = info['formats'].copy() - sorted_formats.sort(key=lambda x: util.default_multi_get(_formats, x['format_id'], 'height', default=0)) - for index, format in enumerate(sorted_formats): - if util.default_multi_get(_formats, format['format_id'], 'height', default=0) >= 360: - break - sorted_formats = sorted_formats[index:] + sorted_formats[0:index] - sorted_formats = [format for format in info['formats'] if format['acodec'] != 'none' and format['vcodec'] != 'none'] - return sorted_formats - -source_tag_template = Template(''' -''') -def formats_html(formats): - result = '' - for format in formats: - result += source_tag_template.substitute( - src=format['url'], - type='audio/' + format['ext'] if format['vcodec'] == "none" else 'video/' + format['ext'], - ) - return result +def get_video_sources(info): + video_sources = [] + for format in info['formats']: + if format['acodec'] != 'none' and format['vcodec'] != 'none': + video_sources.append({ + 'src': format['url'], + 'type': 'video/' + format['ext'], + }) + return video_sources -subtitles_tag_template = Template(''' -''') -def subtitles_html(info): - result = '' +def get_subtitle_sources(info): + sources = [] default_found = False - default = '' + default = None for language, formats in info['subtitles'].items(): for format in formats: if format['ext'] == 'vtt': - append = subtitles_tag_template.substitute( - src = html.escape('/' + format['url']), - label = html.escape(language), - srclang = html.escape(language), - default = 'default' if language == settings.subtitles_language and settings.subtitles_mode > 0 else '', - ) + source = { + 'url': '/' + format['url'], + 'label': language, + 'srclang': language, + + # set as on by default if this is the preferred language and a default-on subtitles mode is in settings + 'on': language == settings.subtitles_language and settings.subtitles_mode > 0, + } + if language == settings.subtitles_language: default_found = True - default = append + default = source else: - result += append + result.append(source) break - result += default + + # Put it at the end to avoid browser bug when there are too many languages + # (in firefox, it is impossible to select a language near the top of the list because it is cut off) + if default_found: + sources.append(default) + try: formats = info['automatic_captions'][settings.subtitles_language] except KeyError: @@ -208,19 +92,60 @@ def subtitles_html(info): else: for format in formats: if format['ext'] == 'vtt': - result += subtitles_tag_template.substitute( - src = html.escape('/' + format['url']), - label = settings.subtitles_language + ' - Automatic', - srclang = settings.subtitles_language, - default = 'default' if settings.subtitles_mode == 2 and not default_found else '', - ) - return result + sources.append({ + 'url': '/' + format['url'], + 'label': settings.subtitles_language + ' - Automatic', + 'srclang': settings.subtitles_language, + + # set as on by default if this is the preferred language and a default-on subtitles mode is in settings + 'on': settings.subtitles_mode == 2 and not default_found, + + }) + + return sources + + +def get_music_list_html(music_list): + if len(music_list) == 0: + music_list_html = '' + else: + # get the set of attributes which are used by atleast 1 track + # so there isn't an empty, extraneous album column which no tracks use, for example + used_attributes = set() + for track in music_list: + used_attributes = used_attributes | track.keys() + + # now put them in the right order + ordered_attributes = [] + for attribute in ('Artist', 'Title', 'Album'): + if attribute.lower() in used_attributes: + ordered_attributes.append(attribute) + + music_list_html = '''
+ + + +''' + # table headings + for attribute in ordered_attributes: + music_list_html += "\n" + music_list_html += '''\n''' + + for track in music_list: + music_list_html += '''\n''' + for attribute in ordered_attributes: + try: + value = track[attribute.lower()] + except KeyError: + music_list_html += '''''' + else: + music_list_html += '''''' + music_list_html += '''\n''' + music_list_html += '''
Music
" + attribute + "
''' + html.escape(value) + '''
\n''' + return music_list_html -more_comments_template = Template('''More comments''') -download_link_template = Template(''' - $ext $resolution $note''') def extract_info(downloader, *args, **kwargs): try: @@ -228,136 +153,87 @@ def extract_info(downloader, *args, **kwargs): except YoutubeError as e: return str(e) -music_list_table_row = Template(''' - $attribute - $value -''') -def get_watch_page(env, start_response): - video_id = env['parameters']['v'][0] - if len(video_id) < 11: - start_response('404 Not Found', [('Content-type', 'text/plain'),]) - return b'Incomplete video id (too short): ' + video_id.encode('ascii') - - start_response('200 OK', [('Content-type','text/html'),]) - - lc = util.default_multi_get(env['parameters'], 'lc', 0, default='') - if settings.route_tor: - proxy = 'socks5://127.0.0.1:9150/' - else: - proxy = '' - downloader = YoutubeDL(params={'youtube_include_dash_manifest':False, 'proxy':proxy}) - tasks = ( - gevent.spawn(comments.video_comments, video_id, int(settings.default_comment_sorting), lc=lc ), - gevent.spawn(extract_info, downloader, "https://www.youtube.com/watch?v=" + video_id, download=False) - ) - gevent.joinall(tasks) - comments_html, info = tasks[0].value, tasks[1].value - - - #comments_html = comments.comments_html(video_id(url)) - #info = YoutubeDL().extract_info(url, download=False) - - #chosen_format = choose_format(info) - - if isinstance(info, str): # youtube error - return html_common.yt_basic_template.substitute( - page_title = "Error", - style = "", - header = html_common.get_header(), - page = html.escape(info), - ).encode('utf-8') - - sorted_formats = sort_formats(info) - - video_info = { - "duration": util.seconds_to_timestamp(info["duration"]), - "id": info['id'], - "title": info['title'], - "author": info['uploader'], - } - - upload_year = info["upload_date"][0:4] - upload_month = info["upload_date"][4:6] - upload_day = info["upload_date"][6:8] - upload_date = upload_month + "/" + upload_day + "/" + upload_year - - if settings.enable_related_videos: - related_videos_html = get_related_items_html(info) - else: - related_videos_html = '' - music_list = info['music_list'] - if len(music_list) == 0: - music_list_html = '' - else: - # get the set of attributes which are used by atleast 1 track - # so there isn't an empty, extraneous album column which no tracks use, for example - used_attributes = set() - for track in music_list: - used_attributes = used_attributes | track.keys() - - # now put them in the right order - ordered_attributes = [] - for attribute in ('Artist', 'Title', 'Album'): - if attribute.lower() in used_attributes: - ordered_attributes.append(attribute) - - music_list_html = '''
- - - -''' - # table headings - for attribute in ordered_attributes: - music_list_html += "\n" - music_list_html += '''\n''' - for track in music_list: - music_list_html += '''\n''' - for attribute in ordered_attributes: - try: - value = track[attribute.lower()] - except KeyError: - music_list_html += '''''' - else: - music_list_html += '''''' - music_list_html += '''\n''' - music_list_html += '''
Music
" + attribute + "
''' + html.escape(value) + '''
\n''' - 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_options = '' - for format in info['formats']: - download_options += download_link_template.substitute( - url = html.escape(format['url']), - ext = html.escape(format['ext']), - resolution = html.escape(downloader.format_resolution(format)), - note = html.escape(downloader._format_note(format)), - ) - - - page = yt_watch_template.substitute( - video_title = html.escape(info["title"]), - page_title = html.escape(info["title"]), - header = html_common.get_header(), - uploader = html.escape(info["uploader"]), - uploader_channel_url = '/' + info["uploader_url"], - upload_date = upload_date, - views = (lambda x: '{:,}'.format(x) if x is not None else "")(info.get("view_count", None)), - likes = (lambda x: '{:,}'.format(x) if x is not None else "")(info.get("like_count", None)), - dislikes = (lambda x: '{:,}'.format(x) if x is not None else "")(info.get("dislike_count", None)), - download_options = download_options, - video_info = html.escape(json.dumps(video_info)), - description = html.escape(info["description"]), - video_sources = formats_html(sorted_formats) + subtitles_html(info), - related = related_videos_html, - - comments = comments_html, - - music_list = music_list_html, - is_unlisted = 'Unlisted' if info['unlisted'] else '', - ) - return page.encode('utf-8') + +@yt_app.route('/watch') +def get_watch_page(): + video_id = request.args['v'] + if len(video_id) < 11: + abort(404) + abort(Response('Incomplete video id (too short): ' + video_id)) + + lc = request.args.get('lc', '') + if settings.route_tor: + proxy = 'socks5://127.0.0.1:9150/' + else: + proxy = '' + yt_dl_downloader = YoutubeDL(params={'youtube_include_dash_manifest':False, 'proxy':proxy}) + tasks = ( + gevent.spawn(comments.video_comments, video_id, int(settings.default_comment_sorting), lc=lc ), + gevent.spawn(extract_info, yt_dl_downloader, "https://www.youtube.com/watch?v=" + video_id, download=False) + ) + gevent.joinall(tasks) + comments_html, info = tasks[0].value, tasks[1].value + + if isinstance(info, str): # youtube error + return flask.render_template('error.html', header = html_common.get_header, error_mesage = info) + + video_info = { + "duration": util.seconds_to_timestamp(info["duration"]), + "id": info['id'], + "title": info['title'], + "author": info['uploader'], + } + + upload_year = info["upload_date"][0:4] + upload_month = info["upload_date"][4:6] + upload_day = info["upload_date"][6:8] + upload_date = upload_month + "/" + upload_day + "/" + upload_year + + if settings.enable_related_videos: + related_videos_html = get_related_items_html(info) + else: + related_videos_html = '' + + + 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 = [] + + for format in info['formats']: + download_formats.append({ + 'url': format['url'], + 'ext': format['ext'], + 'resolution': yt_dl_downloader.format_resolution(format), + 'note': yt_dl_downloader._format_note(format), + }) + + + return flask.render_template('watch.html', + header = html_common.get_header(), + uploader_channel_url = '/' + info['uploader_url'], + upload_date = upload_date, + views = (lambda x: '{:,}'.format(x) if x is not None else "")(info.get("view_count", None)), + likes = (lambda x: '{:,}'.format(x) if x is not None else "")(info.get("like_count", None)), + dislikes = (lambda x: '{:,}'.format(x) if x is not None else "")(info.get("dislike_count", None)), + download_formats = download_formats, + video_info = json.dumps(video_info), + video_sources = get_video_sources(info), + subtitle_sources = get_subtitle_sources(info), + + # TODO: refactor these + related = related_videos_html, + comments = comments_html, + music_list = get_music_list_html(info['music_list']), + + title = info['title'], + uploader = info['uploader'], + description = info['description'], + unlisted = info['unlisted'], + ) diff --git a/youtube/youtube.py b/youtube/youtube.py deleted file mode 100644 index a6a216e..0000000 --- a/youtube/youtube.py +++ /dev/null @@ -1,105 +0,0 @@ -import mimetypes -import urllib.parse -import os -from youtube import local_playlist, watch, search, playlist, channel, comments, post_comment, accounts, util -import settings -YOUTUBE_FILES = ( - "/shared.css", - '/comments.css', - '/favicon.ico', -) -get_handlers = { - 'search': search.get_search_page, - '': search.get_search_page, - 'watch': watch.get_watch_page, - 'playlist': playlist.get_playlist_page, - - 'channel': channel.get_channel_page, - 'user': channel.get_channel_page_general_url, - 'c': channel.get_channel_page_general_url, - - 'playlists': local_playlist.get_playlist_page, - - 'comments': comments.get_comments_page, - 'post_comment': post_comment.get_post_comment_page, - 'delete_comment': post_comment.get_delete_comment_page, - 'login': accounts.get_account_login_page, -} -post_handlers = { - 'edit_playlist': local_playlist.edit_playlist, - 'playlists': local_playlist.path_edit_playlist, - - 'login': accounts.add_account, - 'comments': post_comment.post_comment, - 'post_comment': post_comment.post_comment, - 'delete_comment': post_comment.delete_comment, -} - -def youtube(env, start_response): - path, method, query_string = env['PATH_INFO'], env['REQUEST_METHOD'], env['QUERY_STRING'] - env['qs_parameters'] = urllib.parse.parse_qs(query_string) - env['parameters'] = dict(env['qs_parameters']) - - path_parts = path.rstrip('/').lstrip('/').split('/') - env['path_parts'] = path_parts - - if method == "GET": - try: - handler = get_handlers[path_parts[0]] - except KeyError: - pass - else: - return handler(env, start_response) - - if path in YOUTUBE_FILES: - with open("youtube" + path, 'rb') as f: - mime_type = mimetypes.guess_type(path)[0] or 'application/octet-stream' - start_response('200 OK', (('Content-type',mime_type),) ) - return f.read() - - elif path.startswith("/data/playlist_thumbnails/"): - with open(os.path.join(settings.data_dir, os.path.normpath(path[6:])), 'rb') as f: - start_response('200 OK', (('Content-type', "image/jpeg"),) ) - return f.read() - - elif path.startswith("/api/"): - start_response('200 OK', [('Content-type', 'text/vtt'),] ) - result = util.fetch_url('https://www.youtube.com' + path + ('?' + query_string if query_string else '')) - result = result.replace(b"align:start position:0%", b"") - return result - - elif path == "/opensearch.xml": - with open("youtube" + path, 'rb') as f: - mime_type = mimetypes.guess_type(path)[0] or 'application/octet-stream' - start_response('200 OK', (('Content-type',mime_type),) ) - return f.read().replace(b'$port_number', str(settings.port_number).encode()) - - elif path == "/comment_delete_success": - start_response('200 OK', [('Content-type', 'text/plain'),] ) - return b'Successfully deleted comment' - - elif path == "/comment_delete_fail": - start_response('200 OK', [('Content-type', 'text/plain'),] ) - return b'Failed to deleted comment' - - else: - return channel.get_channel_page_general_url(env, start_response) - - elif method == "POST": - post_parameters = urllib.parse.parse_qs(env['wsgi.input'].read().decode()) - env['post_parameters'] = post_parameters - env['parameters'].update(post_parameters) - - try: - handler = post_handlers[path_parts[0]] - except KeyError: - pass - else: - return handler(env, start_response) - - start_response('404 Not Found', [('Content-type', 'text/plain'),]) - return b'404 Not Found' - - else: - start_response('501 Not Implemented', [('Content-type', 'text/plain'),]) - return b'501 Not Implemented' -- cgit v1.2.3