Add views column and some other database changes.
And various other changes which have been sitting here for too long.
This commit is contained in:
parent
420a14bb88
commit
78ce6a6f41
4 changed files with 125 additions and 56 deletions
|
@ -103,6 +103,7 @@
|
||||||
<img src="http://i3.ytimg.com/vi/{{video['id']}}/default.jpg" height="100px">
|
<img src="http://i3.ytimg.com/vi/{{video['id']}}/default.jpg" height="100px">
|
||||||
<a class="video_title" href="https://www.youtube.com/watch?v={{video['id']}}">{{video['_published_str']}} - {{video['title']}}</a>
|
<a class="video_title" href="https://www.youtube.com/watch?v={{video['id']}}">{{video['_published_str']}} - {{video['title']}}</a>
|
||||||
<span>({{video['duration'] | seconds_to_hms}})</span>
|
<span>({{video['duration'] | seconds_to_hms}})</span>
|
||||||
|
<span>({{video['views']}})</span>
|
||||||
{% if channel is none %}
|
{% if channel is none %}
|
||||||
<a href="/channel/{{video['author_id']}}">(Chan)</a>
|
<a href="/channel/{{video['author_id']}}">(Chan)</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -5,10 +5,49 @@ import sys
|
||||||
|
|
||||||
import ycdl
|
import ycdl
|
||||||
|
|
||||||
|
def upgrade_3_to_4(sql):
|
||||||
|
'''
|
||||||
|
In this version, the views column was added.
|
||||||
|
'''
|
||||||
|
cur = sql.cursor()
|
||||||
|
cur.executescript('''
|
||||||
|
ALTER TABLE videos RENAME TO videos_old;
|
||||||
|
CREATE TABLE videos(
|
||||||
|
id TEXT,
|
||||||
|
published INT,
|
||||||
|
author_id TEXT,
|
||||||
|
title TEXT,
|
||||||
|
description TEXT,
|
||||||
|
duration INT,
|
||||||
|
views INT,
|
||||||
|
thumbnail TEXT,
|
||||||
|
download TEXT
|
||||||
|
);
|
||||||
|
INSERT INTO videos SELECT
|
||||||
|
id,
|
||||||
|
published,
|
||||||
|
author_id,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
duration,
|
||||||
|
NULL,
|
||||||
|
thumbnail,
|
||||||
|
download
|
||||||
|
FROM videos_old;
|
||||||
|
DROP TABLE videos_old;
|
||||||
|
''')
|
||||||
|
|
||||||
|
def upgrade_2_to_3(sql):
|
||||||
|
'''
|
||||||
|
In this version, a column `automark` was added to the channels table, where
|
||||||
|
you can set channels to automatically mark videos as ignored or downloaded.
|
||||||
|
'''
|
||||||
|
cur = sql.cursor()
|
||||||
|
cur.execute('ALTER TABLE channels ADD COLUMN automark TEXT')
|
||||||
|
|
||||||
def upgrade_1_to_2(sql):
|
def upgrade_1_to_2(sql):
|
||||||
'''
|
'''
|
||||||
In this version, a column `tagged_at` was added to the Photos table, to keep
|
In this version, the duration column was added.
|
||||||
track of the last time the photo's tags were edited (added or removed).
|
|
||||||
'''
|
'''
|
||||||
cur = sql.cursor()
|
cur = sql.cursor()
|
||||||
cur.executescript('''
|
cur.executescript('''
|
||||||
|
|
101
ycdl/ycdl.py
101
ycdl/ycdl.py
|
@ -18,27 +18,7 @@ logging.getLogger('googleapiclient.discovery').setLevel(logging.WARNING)
|
||||||
logging.getLogger('requests.packages.urllib3.connectionpool').setLevel(logging.WARNING)
|
logging.getLogger('requests.packages.urllib3.connectionpool').setLevel(logging.WARNING)
|
||||||
logging.getLogger('requests.packages.urllib3.util.retry').setLevel(logging.WARNING)
|
logging.getLogger('requests.packages.urllib3.util.retry').setLevel(logging.WARNING)
|
||||||
|
|
||||||
SQL_CHANNEL_COLUMNS = [
|
DATABASE_VERSION = 4
|
||||||
'id',
|
|
||||||
'name',
|
|
||||||
'directory',
|
|
||||||
]
|
|
||||||
|
|
||||||
SQL_VIDEO_COLUMNS = [
|
|
||||||
'id',
|
|
||||||
'published',
|
|
||||||
'author_id',
|
|
||||||
'title',
|
|
||||||
'description',
|
|
||||||
'duration',
|
|
||||||
'thumbnail',
|
|
||||||
'download',
|
|
||||||
]
|
|
||||||
|
|
||||||
SQL_CHANNEL = {key:index for (index, key) in enumerate(SQL_CHANNEL_COLUMNS)}
|
|
||||||
SQL_VIDEO = {key:index for (index, key) in enumerate(SQL_VIDEO_COLUMNS)}
|
|
||||||
|
|
||||||
DATABASE_VERSION = 2
|
|
||||||
DB_INIT = '''
|
DB_INIT = '''
|
||||||
PRAGMA count_changes = OFF;
|
PRAGMA count_changes = OFF;
|
||||||
PRAGMA cache_size = 10000;
|
PRAGMA cache_size = 10000;
|
||||||
|
@ -46,7 +26,8 @@ PRAGMA user_version = {user_version};
|
||||||
CREATE TABLE IF NOT EXISTS channels(
|
CREATE TABLE IF NOT EXISTS channels(
|
||||||
id TEXT,
|
id TEXT,
|
||||||
name TEXT,
|
name TEXT,
|
||||||
directory TEXT COLLATE NOCASE
|
directory TEXT COLLATE NOCASE,
|
||||||
|
automark TEXT
|
||||||
);
|
);
|
||||||
CREATE TABLE IF NOT EXISTS videos(
|
CREATE TABLE IF NOT EXISTS videos(
|
||||||
id TEXT,
|
id TEXT,
|
||||||
|
@ -55,6 +36,7 @@ CREATE TABLE IF NOT EXISTS videos(
|
||||||
title TEXT,
|
title TEXT,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
duration INT,
|
duration INT,
|
||||||
|
views INT,
|
||||||
thumbnail TEXT,
|
thumbnail TEXT,
|
||||||
download TEXT
|
download TEXT
|
||||||
);
|
);
|
||||||
|
@ -67,12 +49,34 @@ CREATE INDEX IF NOT EXISTS index_video_published on videos(published);
|
||||||
CREATE INDEX IF NOT EXISTS index_video_download on videos(download);
|
CREATE INDEX IF NOT EXISTS index_video_download on videos(download);
|
||||||
'''.format(user_version=DATABASE_VERSION)
|
'''.format(user_version=DATABASE_VERSION)
|
||||||
|
|
||||||
|
SQL_CHANNEL_COLUMNS = [
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'directory',
|
||||||
|
'automark',
|
||||||
|
]
|
||||||
|
|
||||||
|
SQL_VIDEO_COLUMNS = [
|
||||||
|
'id',
|
||||||
|
'published',
|
||||||
|
'author_id',
|
||||||
|
'title',
|
||||||
|
'description',
|
||||||
|
'duration',
|
||||||
|
'views',
|
||||||
|
'thumbnail',
|
||||||
|
'download',
|
||||||
|
]
|
||||||
|
|
||||||
|
SQL_CHANNEL = {key:index for (index, key) in enumerate(SQL_CHANNEL_COLUMNS)}
|
||||||
|
SQL_VIDEO = {key:index for (index, key) in enumerate(SQL_VIDEO_COLUMNS)}
|
||||||
|
|
||||||
DEFAULT_DBNAME = 'ycdl.db'
|
DEFAULT_DBNAME = 'ycdl.db'
|
||||||
|
|
||||||
ERROR_DATABASE_OUTOFDATE = 'Database is out-of-date. {current} should be {new}'
|
ERROR_DATABASE_OUTOFDATE = 'Database is out-of-date. {current} should be {new}.'
|
||||||
|
|
||||||
|
|
||||||
def verify_is_abspath(path):
|
def assert_is_abspath(path):
|
||||||
'''
|
'''
|
||||||
TO DO: Determine whether this is actually correct.
|
TO DO: Determine whether this is actually correct.
|
||||||
'''
|
'''
|
||||||
|
@ -86,7 +90,6 @@ class InvalidVideoState(Exception):
|
||||||
class NoSuchVideo(Exception):
|
class NoSuchVideo(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class YCDL:
|
class YCDL:
|
||||||
def __init__(self, youtube, database_filename=None, youtube_dl_function=None):
|
def __init__(self, youtube, database_filename=None, youtube_dl_function=None):
|
||||||
self.youtube = youtube
|
self.youtube = youtube
|
||||||
|
@ -119,6 +122,7 @@ class YCDL:
|
||||||
def add_channel(
|
def add_channel(
|
||||||
self,
|
self,
|
||||||
channel_id,
|
channel_id,
|
||||||
|
*,
|
||||||
commit=True,
|
commit=True,
|
||||||
download_directory=None,
|
download_directory=None,
|
||||||
get_videos=False,
|
get_videos=False,
|
||||||
|
@ -134,27 +138,24 @@ class YCDL:
|
||||||
data[SQL_CHANNEL['id']] = channel_id
|
data[SQL_CHANNEL['id']] = channel_id
|
||||||
data[SQL_CHANNEL['name']] = name
|
data[SQL_CHANNEL['name']] = name
|
||||||
if download_directory is not None:
|
if download_directory is not None:
|
||||||
verify_is_abspath(download_directory)
|
assert_is_abspath(download_directory)
|
||||||
data[SQL_CHANNEL['directory']] = download_directory
|
data[SQL_CHANNEL['directory']] = download_directory
|
||||||
|
|
||||||
self.cur.execute('INSERT INTO channels VALUES(?, ?, ?)', data)
|
self.cur.execute('INSERT INTO channels VALUES(?, ?, ?, ?)', data)
|
||||||
|
|
||||||
if get_videos:
|
if get_videos:
|
||||||
self.refresh_channel(channel_id, commit=False)
|
self.refresh_channel(channel_id, commit=False)
|
||||||
|
|
||||||
if commit:
|
if commit:
|
||||||
self.sql.commit()
|
self.sql.commit()
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
def channel_has_pending(self, channel_id):
|
def channel_has_pending(self, channel_id):
|
||||||
query = 'SELECT * FROM videos WHERE author_id == ? AND download == "pending"'
|
query = 'SELECT 1 FROM videos WHERE author_id == ? AND download == "pending" LIMIT 1'
|
||||||
self.cur.execute(query, [channel_id])
|
self.cur.execute(query, [channel_id])
|
||||||
return self.cur.fetchone() is not None
|
return self.cur.fetchone() is not None
|
||||||
|
|
||||||
def channel_directory(self, channel_id):
|
|
||||||
self.cur.execute('SELECT * FROM channels WHERE id == ?', [channel_id])
|
|
||||||
fetch = self.cur.fetchone()
|
|
||||||
if fetch is None:
|
|
||||||
return None
|
|
||||||
return fetch[SQL_CHANNEL['directory']]
|
|
||||||
|
|
||||||
def download_video(self, video, commit=True, force=False):
|
def download_video(self, video, commit=True, force=False):
|
||||||
'''
|
'''
|
||||||
Execute the `YOUTUBE_DL_COMMAND`, within the channel's associated
|
Execute the `YOUTUBE_DL_COMMAND`, within the channel's associated
|
||||||
|
@ -187,7 +188,7 @@ class YCDL:
|
||||||
return
|
return
|
||||||
|
|
||||||
current_directory = os.getcwd()
|
current_directory = os.getcwd()
|
||||||
download_directory = self.channel_directory(channel_id)
|
download_directory = self.get_channel(channel_id)['directory']
|
||||||
download_directory = download_directory or current_directory
|
download_directory = download_directory or current_directory
|
||||||
|
|
||||||
os.makedirs(download_directory, exist_ok=True)
|
os.makedirs(download_directory, exist_ok=True)
|
||||||
|
@ -269,6 +270,7 @@ class YCDL:
|
||||||
'title': video.title,
|
'title': video.title,
|
||||||
'description': video.description,
|
'description': video.description,
|
||||||
'duration': video.duration,
|
'duration': video.duration,
|
||||||
|
'views': video.views,
|
||||||
'thumbnail': video.thumbnail['url'],
|
'thumbnail': video.thumbnail['url'],
|
||||||
'download': download_status,
|
'download': download_status,
|
||||||
}
|
}
|
||||||
|
@ -302,16 +304,35 @@ class YCDL:
|
||||||
|
|
||||||
def refresh_all_channels(self, force=False, commit=True):
|
def refresh_all_channels(self, force=False, commit=True):
|
||||||
for channel in self.get_channels():
|
for channel in self.get_channels():
|
||||||
self.refresh_channel(channel['id'], force=force, commit=commit)
|
self.refresh_channel(channel, force=force, commit=commit)
|
||||||
if commit:
|
if commit:
|
||||||
self.sql.commit()
|
self.sql.commit()
|
||||||
|
|
||||||
def refresh_channel(self, channel_id, force=False, commit=True):
|
def refresh_channel(self, channel, force=False, commit=True):
|
||||||
video_generator = self.youtube.get_user_videos(uid=channel_id)
|
if isinstance(channel, str):
|
||||||
log.debug('Refreshing channel: %s', channel_id)
|
channel = self.get_channel(channel)
|
||||||
|
|
||||||
|
seen_ids = set()
|
||||||
|
video_generator = self.youtube.get_user_videos(uid=channel['id'])
|
||||||
|
log.debug('Refreshing channel: %s', channel['id'])
|
||||||
for video in video_generator:
|
for video in video_generator:
|
||||||
|
seen_ids.add(video.id)
|
||||||
status = self.insert_video(video, commit=False)
|
status = self.insert_video(video, commit=False)
|
||||||
|
|
||||||
|
if status['new'] and channel['automark'] is not None:
|
||||||
|
self.mark_video_state(video.id, channel['automark'], commit=False)
|
||||||
|
if channel['automark'] == 'downloaded':
|
||||||
|
self.download_video(video.id, commit=False)
|
||||||
|
|
||||||
if not force and not status['new']:
|
if not force and not status['new']:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
if force:
|
||||||
|
known_videos = self.get_videos(channel_id=channel['id'])
|
||||||
|
known_ids = {v['id'] for v in known_videos}
|
||||||
|
refresh_ids = list(known_ids.difference(seen_ids))
|
||||||
|
for video in self.youtube.get_video(refresh_ids):
|
||||||
|
self.insert_video(video, commit=False)
|
||||||
|
|
||||||
if commit:
|
if commit:
|
||||||
self.sql.commit()
|
self.sql.commit()
|
||||||
|
|
|
@ -21,6 +21,7 @@ class Video:
|
||||||
|
|
||||||
snippet = data['snippet']
|
snippet = data['snippet']
|
||||||
content_details = data['contentDetails']
|
content_details = data['contentDetails']
|
||||||
|
statistics = data['statistics']
|
||||||
|
|
||||||
self.title = snippet['title'] or '[untitled]'
|
self.title = snippet['title'] or '[untitled]'
|
||||||
self.description = snippet['description']
|
self.description = snippet['description']
|
||||||
|
@ -33,6 +34,7 @@ class Video:
|
||||||
self.published = published.timestamp()
|
self.published = published.timestamp()
|
||||||
|
|
||||||
self.duration = isodate.parse_duration(content_details['duration']).seconds
|
self.duration = isodate.parse_duration(content_details['duration']).seconds
|
||||||
|
self.views = statistics['viewCount']
|
||||||
|
|
||||||
thumbnails = snippet['thumbnails']
|
thumbnails = snippet['thumbnails']
|
||||||
best_thumbnail = max(thumbnails, key=lambda x: thumbnails[x]['width'] * thumbnails[x]['height'])
|
best_thumbnail = max(thumbnails, key=lambda x: thumbnails[x]['width'] * thumbnails[x]['height'])
|
||||||
|
@ -66,7 +68,6 @@ class Youtube:
|
||||||
user = self.youtube.channels().list(part='contentDetails', id=uid).execute()
|
user = self.youtube.channels().list(part='contentDetails', id=uid).execute()
|
||||||
upload_playlist = user['items'][0]['contentDetails']['relatedPlaylists']['uploads']
|
upload_playlist = user['items'][0]['contentDetails']['relatedPlaylists']['uploads']
|
||||||
page_token = None
|
page_token = None
|
||||||
total = 0
|
|
||||||
while True:
|
while True:
|
||||||
response = self.youtube.playlistItems().list(
|
response = self.youtube.playlistItems().list(
|
||||||
maxResults=50,
|
maxResults=50,
|
||||||
|
@ -78,12 +79,11 @@ class Youtube:
|
||||||
video_ids = [item['contentDetails']['videoId'] for item in response['items']]
|
video_ids = [item['contentDetails']['videoId'] for item in response['items']]
|
||||||
videos = self.get_video(video_ids)
|
videos = self.get_video(video_ids)
|
||||||
videos.sort(key=lambda x: x.published, reverse=True)
|
videos.sort(key=lambda x: x.published, reverse=True)
|
||||||
yield from videos
|
|
||||||
|
|
||||||
count = len(videos)
|
for video in videos:
|
||||||
total += count
|
yield video
|
||||||
print(f'Found {count} more, {total} total')
|
|
||||||
if page_token is None or count < 50:
|
if page_token is None:
|
||||||
break
|
break
|
||||||
|
|
||||||
def get_related_videos(self, video_id, count=50):
|
def get_related_videos(self, video_id, count=50):
|
||||||
|
@ -108,17 +108,25 @@ class Youtube:
|
||||||
else:
|
else:
|
||||||
singular = False
|
singular = False
|
||||||
|
|
||||||
results = []
|
snippets = []
|
||||||
chunks = helpers.chunk_sequence(video_ids, 50)
|
chunks = helpers.chunk_sequence(video_ids, 50)
|
||||||
for chunk in chunks:
|
for chunk in chunks:
|
||||||
chunk = ','.join(chunk)
|
chunk = ','.join(chunk)
|
||||||
data = self.youtube.videos().list(part='id,contentDetails,snippet', id=chunk).execute()
|
data = self.youtube.videos().list(part='id,contentDetails,snippet,statistics', id=chunk).execute()
|
||||||
items = data['items']
|
items = data['items']
|
||||||
results.extend(items)
|
snippets.extend(items)
|
||||||
results = [Video(snippet) for snippet in results]
|
videos = []
|
||||||
|
broken = []
|
||||||
|
for snippet in snippets:
|
||||||
|
try:
|
||||||
|
videos.append(Video(snippet))
|
||||||
|
except KeyError:
|
||||||
|
broken.append(snippet)
|
||||||
|
if broken:
|
||||||
|
print('broken:', broken)
|
||||||
if singular:
|
if singular:
|
||||||
if len(results) == 1:
|
if len(videos) == 1:
|
||||||
return results[0]
|
return videos[0]
|
||||||
elif len(results) == 0:
|
elif len(videos) == 0:
|
||||||
raise VideoNotFound(video_ids[0])
|
raise VideoNotFound(video_ids[0])
|
||||||
return results
|
return videos
|
||||||
|
|
Loading…
Reference in a new issue