Rearrangements, new route and minor changes
This commit is contained in:
113
backend.py
113
backend.py
@@ -1,19 +1,27 @@
|
|||||||
import threading
|
import threading
|
||||||
|
|
||||||
from flask import g
|
|
||||||
import yt_dlp as ydl
|
import yt_dlp as ydl
|
||||||
from yt_dlp import DownloadError
|
from yt_dlp import DownloadError
|
||||||
import os
|
import os
|
||||||
import zipfile
|
import zipfile
|
||||||
import sqlite3
|
|
||||||
from base64 import b64encode
|
|
||||||
|
|
||||||
|
from base64 import b64encode
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
from db_tools import *
|
||||||
from file_cache import *
|
from file_cache import *
|
||||||
|
|
||||||
|
|
||||||
|
# adds thread to queue and starts it
|
||||||
|
def enqueue_download(url):
|
||||||
|
# download and processing is happening in background / another thread
|
||||||
|
t = Thread(target=process_download, args=(url,))
|
||||||
|
thread_queue.append(t)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
|
||||||
# this is the 'controller' for the download process
|
# this is the 'controller' for the download process
|
||||||
def process_download(url):
|
def process_download(url):
|
||||||
# wait for previous thread if not first in list
|
# wait for previous thread to finish if not first / only in list
|
||||||
current_thread = threading.current_thread()
|
current_thread = threading.current_thread()
|
||||||
if len(thread_queue) > 0 and thread_queue[0] is not current_thread:
|
if len(thread_queue) > 0 and thread_queue[0] is not current_thread:
|
||||||
threading.Thread.join(thread_queue[thread_queue.index(current_thread) - 1])
|
threading.Thread.join(thread_queue[thread_queue.index(current_thread) - 1])
|
||||||
@@ -35,9 +43,8 @@ def process_download(url):
|
|||||||
try:
|
try:
|
||||||
# this throws KeyError when downloading single file
|
# this throws KeyError when downloading single file
|
||||||
for video in query['entries']:
|
for video in query['entries']:
|
||||||
if check_already_exists(video['id']): # todo: this shit aint tested
|
if check_already_exists(video['id']):
|
||||||
query_db_threaded('INSERT INTO collection(playlist, video) VALUES (:folder, :id)',
|
add_to_collection_if_not_added(parent, video['id'])
|
||||||
{'folder': parent + '\\', 'id': video['id']})
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# this throws DownloadError when not downloading playlist
|
# this throws DownloadError when not downloading playlist
|
||||||
@@ -50,6 +57,7 @@ def process_download(url):
|
|||||||
|
|
||||||
# start download
|
# start download
|
||||||
download_all(parent)
|
download_all(parent)
|
||||||
|
thread_queue.remove(current_thread)
|
||||||
return
|
return
|
||||||
|
|
||||||
# when downloading: channel: DownloadError, single file: KeyError
|
# when downloading: channel: DownloadError, single file: KeyError
|
||||||
@@ -63,19 +71,19 @@ def process_download(url):
|
|||||||
# for every video in their respective tabs
|
# for every video in their respective tabs
|
||||||
for video in tab['entries']:
|
for video in tab['entries']:
|
||||||
if check_already_exists(video['id']):
|
if check_already_exists(video['id']):
|
||||||
query_db_threaded('INSERT INTO collection(playlist, video) VALUES (:folder, :id)',
|
add_to_collection_if_not_added(parent, video['id'])
|
||||||
{'folder': parent + '\\', 'id': video['id']})
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# todo: there have been cases of duplicate urls or some with '/watch?v=@channel_name'
|
# there have been cases of duplicate urls or some with '/watch?v=@channel_name'
|
||||||
# but no consistency has been observed
|
# but no consistency has been observed
|
||||||
# still works though so will not be checked for now
|
# still works though so will not be checked for now
|
||||||
ids.append(video['id'])
|
ids.append(video['id'])
|
||||||
titles.append(video['title'])
|
titles.append(video['title'])
|
||||||
urls.append('https://www.youtube.com/watch?v=' + video['id'])
|
urls.append('https://www.youtube.com/watch?v=' + video['id'])
|
||||||
|
|
||||||
# start download
|
# start download
|
||||||
download_all(parent)
|
download_all(parent)
|
||||||
|
thread_queue.remove(current_thread)
|
||||||
return
|
return
|
||||||
|
|
||||||
# when downloading single file: KeyError
|
# when downloading single file: KeyError
|
||||||
@@ -92,7 +100,6 @@ def process_download(url):
|
|||||||
|
|
||||||
# start download
|
# start download
|
||||||
download_all()
|
download_all()
|
||||||
return
|
|
||||||
|
|
||||||
# this is broad on purpose; there has been no exception thrown here _yet_
|
# this is broad on purpose; there has been no exception thrown here _yet_
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -100,7 +107,6 @@ def process_download(url):
|
|||||||
|
|
||||||
# todo: a site with (not) finished downloads (url/datetime) would be nice so you know when it's done
|
# todo: a site with (not) finished downloads (url/datetime) would be nice so you know when it's done
|
||||||
# downloading large playlists does take quite a while after all
|
# downloading large playlists does take quite a while after all
|
||||||
# adding that entry to the site would be done -here- i guess
|
|
||||||
|
|
||||||
thread_queue.remove(current_thread)
|
thread_queue.remove(current_thread)
|
||||||
return
|
return
|
||||||
@@ -108,70 +114,13 @@ def process_download(url):
|
|||||||
|
|
||||||
# checks whether a video is already in db
|
# checks whether a video is already in db
|
||||||
def check_already_exists(video_id) -> bool:
|
def check_already_exists(video_id) -> bool:
|
||||||
res = query_db_threaded('SELECT name FROM video WHERE id = :id', {'id': video_id})
|
res = query_db_threaded('SELECT name FROM video WHERE id = :id',
|
||||||
|
{'id': video_id})
|
||||||
if len(res) > 0:
|
if len(res) > 0:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
# fetches db from app context
|
|
||||||
def get_db():
|
|
||||||
db = getattr(g, '_database', None)
|
|
||||||
if db is None:
|
|
||||||
db = g._database = sqlite3.connect('files.sqlite')
|
|
||||||
db.row_factory = sqlite3.Row
|
|
||||||
return db
|
|
||||||
|
|
||||||
|
|
||||||
# used when accessing db from app context; keeps connection alive since it's used more frequently
|
|
||||||
def query_db(query, args=(), one=False):
|
|
||||||
db = get_db()
|
|
||||||
cur = db.execute(query, args)
|
|
||||||
res = cur.fetchall()
|
|
||||||
db.commit()
|
|
||||||
cur.close()
|
|
||||||
return (res[0] if res else None) if one else res
|
|
||||||
|
|
||||||
|
|
||||||
# used when accessing db from thread, since app context is thread local; does not keep connection alive
|
|
||||||
def query_db_threaded(query, args=(), one=False):
|
|
||||||
db = sqlite3.connect('files.sqlite')
|
|
||||||
cur = db.execute(query, args)
|
|
||||||
res = cur.fetchall()
|
|
||||||
db.commit()
|
|
||||||
cur.close()
|
|
||||||
db.close()
|
|
||||||
return (res[0] if res else None) if one else res
|
|
||||||
|
|
||||||
|
|
||||||
# add entries do db
|
|
||||||
def db_add(ext, parent_rowid=None, parent=None):
|
|
||||||
# if no parent was specified
|
|
||||||
if parent is None:
|
|
||||||
# insert video into db
|
|
||||||
query_db_threaded('INSERT INTO video(id, name, ext, path) VALUES (:id, :name, :ext, :path)',
|
|
||||||
{'id': ids[0], 'name': titles[0], 'ext': '.' + ext, 'path': '\\'})
|
|
||||||
|
|
||||||
# if a parent was specified
|
|
||||||
else:
|
|
||||||
# set relative path
|
|
||||||
relative_path = parent + '\\'
|
|
||||||
|
|
||||||
# if a rowid was specified
|
|
||||||
if parent_rowid is not None:
|
|
||||||
# adjust the relative path
|
|
||||||
relative_path += str(parent_rowid) + '\\'
|
|
||||||
|
|
||||||
# insert all new files into db
|
|
||||||
for i in range(len(titles)):
|
|
||||||
query_db_threaded('INSERT INTO video(id, name, ext, path) VALUES (:id, :name, :ext, :path)',
|
|
||||||
{'id': ids[i], 'name': titles[i], 'ext': '.' + ext, 'path': relative_path})
|
|
||||||
query_db_threaded('INSERT INTO collection(playlist, video) VALUES (:folder, :id)',
|
|
||||||
{'folder': relative_path, 'id': ids[i]})
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def download_all(parent=None, ext='mp3'):
|
def download_all(parent=None, ext='mp3'):
|
||||||
# if no new files to download, there's nothing to do here
|
# if no new files to download, there's nothing to do here
|
||||||
if not len(urls) > 0: return
|
if not len(urls) > 0: return
|
||||||
@@ -245,9 +194,20 @@ def download_all(parent=None, ext='mp3'):
|
|||||||
else:
|
else:
|
||||||
location = downloads_path()
|
location = downloads_path()
|
||||||
|
|
||||||
|
# start actual file download
|
||||||
|
yt_download(location, ext)
|
||||||
|
|
||||||
|
# add downloaded files to db
|
||||||
|
db_add(ext, rowid_new, parent)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
# actually downloads files
|
||||||
|
def yt_download(location, ext='mp3'):
|
||||||
# base download options for audio
|
# base download options for audio
|
||||||
opts = {
|
opts = {
|
||||||
'quiet': False,
|
'quiet': True,
|
||||||
'windowsfilenames': True,
|
'windowsfilenames': True,
|
||||||
'outtmpl': location + '%(title)s.%(ext)s',
|
'outtmpl': location + '%(title)s.%(ext)s',
|
||||||
'format': 'bestaudio/best',
|
'format': 'bestaudio/best',
|
||||||
@@ -269,9 +229,6 @@ def download_all(parent=None, ext='mp3'):
|
|||||||
except DownloadError:
|
except DownloadError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# add downloaded files to db
|
|
||||||
db_add(ext, rowid_new, parent)
|
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
70
db_tools.py
Normal file
70
db_tools.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
from flask import g
|
||||||
|
import sqlite3
|
||||||
|
from file_cache import ids, titles
|
||||||
|
|
||||||
|
|
||||||
|
# fetches db from app context
|
||||||
|
def get_db():
|
||||||
|
db = getattr(g, '_database', None)
|
||||||
|
if db is None:
|
||||||
|
db = g._database = sqlite3.connect('files.sqlite')
|
||||||
|
db.row_factory = sqlite3.Row
|
||||||
|
return db
|
||||||
|
|
||||||
|
|
||||||
|
# used when accessing db from app context; keeps connection alive since it's used more frequently
|
||||||
|
def query_db(query, args=(), one=False):
|
||||||
|
db = get_db()
|
||||||
|
cur = db.execute(query, args)
|
||||||
|
res = cur.fetchall()
|
||||||
|
db.commit()
|
||||||
|
cur.close()
|
||||||
|
return (res[0] if res else None) if one else res
|
||||||
|
|
||||||
|
|
||||||
|
# used when accessing db from thread, since app context is thread local; does not keep connection alive
|
||||||
|
def query_db_threaded(query, args=(), one=False):
|
||||||
|
db = sqlite3.connect('files.sqlite')
|
||||||
|
cur = db.execute(query, args)
|
||||||
|
res = cur.fetchall()
|
||||||
|
db.commit()
|
||||||
|
cur.close()
|
||||||
|
db.close()
|
||||||
|
return (res[0] if res else None) if one else res
|
||||||
|
|
||||||
|
|
||||||
|
# add entries do db
|
||||||
|
def db_add(ext, parent_rowid=None, parent=None):
|
||||||
|
# if no parent was specified
|
||||||
|
if parent is None:
|
||||||
|
# insert video into db
|
||||||
|
query_db_threaded('INSERT INTO video(id, name, ext, path) VALUES (:id, :name, :ext, :path)',
|
||||||
|
{'id': ids[0], 'name': titles[0], 'ext': '.' + ext, 'path': '\\'})
|
||||||
|
|
||||||
|
# if a parent was specified
|
||||||
|
else:
|
||||||
|
# set relative path
|
||||||
|
relative_path = parent + '\\'
|
||||||
|
|
||||||
|
# if a rowid was specified
|
||||||
|
if parent_rowid is not None:
|
||||||
|
# adjust the relative path
|
||||||
|
relative_path += str(parent_rowid) + '\\'
|
||||||
|
|
||||||
|
# insert all new files into db
|
||||||
|
for i in range(len(titles)):
|
||||||
|
query_db_threaded('INSERT INTO video(id, name, ext, path) VALUES (:id, :name, :ext, :path)',
|
||||||
|
{'id': ids[i], 'name': titles[i], 'ext': '.' + ext, 'path': relative_path})
|
||||||
|
query_db_threaded('INSERT INTO collection(playlist, video) VALUES (:folder, :id)',
|
||||||
|
{'folder': relative_path, 'id': ids[i]})
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def add_to_collection_if_not_added(parent, video_id):
|
||||||
|
exists = query_db_threaded('SELECT * FROM collection WHERE playlist = :folder AND video = :id',
|
||||||
|
{'folder': parent + '\\', 'id': video_id})
|
||||||
|
|
||||||
|
if not len(exists) > 0:
|
||||||
|
query_db_threaded('INSERT INTO collection(playlist, video) VALUES (:folder, :id)',
|
||||||
|
{'folder': parent + '\\', 'id': video_id})
|
||||||
31
frontend.py
31
frontend.py
@@ -1,13 +1,12 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from threading import Thread
|
|
||||||
|
|
||||||
from flask import Blueprint, request, render_template, flash, send_from_directory, current_app
|
from flask import Blueprint, request, render_template, flash, send_from_directory, current_app
|
||||||
from flask_nav3 import Nav
|
from flask_nav3 import Nav
|
||||||
from flask_nav3.elements import Navbar, View
|
from flask_nav3.elements import Navbar, View
|
||||||
|
|
||||||
from forms.download import DownloadForm
|
from forms.download import DownloadForm
|
||||||
from backend import query_db, process_download, zip_folder, downloads_path
|
from backend import zip_folder, downloads_path, enqueue_download
|
||||||
|
from db_tools import query_db
|
||||||
from file_cache import *
|
from file_cache import *
|
||||||
|
|
||||||
frontend = Blueprint('frontend', __name__)
|
frontend = Blueprint('frontend', __name__)
|
||||||
@@ -50,14 +49,12 @@ def downloader():
|
|||||||
return render_template('downloader.html', form=form, ytLink=valid_link, titles=titles, urls=urls,
|
return render_template('downloader.html', form=form, ytLink=valid_link, titles=titles, urls=urls,
|
||||||
amount=len(titles))
|
amount=len(titles))
|
||||||
|
|
||||||
# download and processing is happening in background / another thread
|
# kick off download process
|
||||||
t = Thread(target=process_download, args=(url,))
|
enqueue_download(url)
|
||||||
thread_queue.append(t)
|
|
||||||
t.start()
|
|
||||||
|
|
||||||
# show download start confirmation
|
# show download start confirmation
|
||||||
flash('Download started and will continue in background.')
|
flash('Download enqueued and will finish in background.')
|
||||||
return render_template('new-downloads.html', titles=titles, urls=urls, amount=len(titles))
|
return render_template('feedback-simple.html', titles=titles, urls=urls, amount=len(titles))
|
||||||
|
|
||||||
|
|
||||||
# downloads a single file
|
# downloads a single file
|
||||||
@@ -81,7 +78,21 @@ def download(file_path):
|
|||||||
|
|
||||||
@frontend.route('/update', methods=['GET', 'POST'])
|
@frontend.route('/update', methods=['GET', 'POST'])
|
||||||
def updater():
|
def updater():
|
||||||
return render_template('updater.html')
|
downloads = query_db('SELECT name, url FROM updatelist INNER JOIN playlist ON updatelist.ROWID = playlist.ROWID')
|
||||||
|
return render_template('updater.html', downloads=downloads)
|
||||||
|
|
||||||
|
|
||||||
|
@frontend.route('/update/<int:url_rowid>')
|
||||||
|
def update(url_rowid):
|
||||||
|
url = query_db('SELECT url FROM updatelist WHERE ROWID = :url_rowid',
|
||||||
|
{'url_rowid': url_rowid})[0][0]
|
||||||
|
|
||||||
|
# kick off download process
|
||||||
|
enqueue_download(url)
|
||||||
|
|
||||||
|
# show download start confirmation
|
||||||
|
flash('Update enqueued and will finish in background.')
|
||||||
|
return render_template('feedback-simple.html', titles=titles, urls=urls, amount=len(titles))
|
||||||
|
|
||||||
|
|
||||||
@frontend.route('/library', methods=['GET'])
|
@frontend.route('/library', methods=['GET'])
|
||||||
|
|||||||
@@ -42,3 +42,9 @@ CREATE TABLE IF NOT EXISTS collection (
|
|||||||
video TEXT NOT NULL
|
video TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
- user-input url, only playlists get added
|
||||||
|
*/
|
||||||
|
CREATE TABLE IF NOT EXISTS updatelist (
|
||||||
|
url TEXT PRIMARY KEY NOT NULL
|
||||||
|
);
|
||||||
|
|||||||
@@ -3,14 +3,33 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{%- with messages = get_flashed_messages(with_categories=True) %}
|
{% if downloads %}
|
||||||
{%- if messages %}
|
<div class="card" style="width: 100%">
|
||||||
<div class="row">
|
<ul class="list-group list-group-flush">
|
||||||
<div class="col-md-12">
|
<li class="list-group-item">
|
||||||
{{utils.flashed_messages(messages)}}
|
<div class="row">
|
||||||
|
<div class="col-md-5">
|
||||||
|
<table id="videos" class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="text-center">Title</th>
|
||||||
|
<th scope="col" class="text-center">Download</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for d in downloads %}
|
||||||
|
<tr>
|
||||||
|
<td class="text-center">{{ d[0] }}</td>
|
||||||
|
<td class="text-center"><a href="/download/{{ video['path'] + video['name'] + video['ext'] }}" download>Link</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</li>
|
||||||
{%- endif %}
|
</ul>
|
||||||
{%- endwith %}
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{%- endblock %}
|
{%- endblock %}
|
||||||
Reference in New Issue
Block a user