Alpha status reached.
Downloads get run in their own threads which are queued. Downloads of single files as well as zips of playlists are possible. Interaction with db has shown no problems.
This commit is contained in:
4
app.py
4
app.py
@@ -2,7 +2,8 @@ from flask import Flask, current_app, g
|
||||
from flask_bootstrap import Bootstrap
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
|
||||
from frontend import frontend, get_db, nav
|
||||
from frontend import frontend, nav
|
||||
from backend import get_db
|
||||
from os import urandom
|
||||
|
||||
|
||||
@@ -19,6 +20,7 @@ def create_app():
|
||||
|
||||
app.config['BOOTSTRAP_SERVE_LOCAL'] = True
|
||||
app.config['SECRET_KEY'] = urandom(32)
|
||||
app.config['BOOTSTRAP_USE_MINIFIED'] = False
|
||||
CSRFProtect(app)
|
||||
|
||||
app.register_blueprint(frontend)
|
||||
|
||||
328
backend.py
Normal file
328
backend.py
Normal file
@@ -0,0 +1,328 @@
|
||||
import threading
|
||||
|
||||
from flask import g
|
||||
import yt_dlp as ydl
|
||||
from yt_dlp import DownloadError
|
||||
import os
|
||||
import zipfile
|
||||
import sqlite3
|
||||
from base64 import b64encode
|
||||
|
||||
from file_cache import *
|
||||
|
||||
|
||||
# this is the 'controller' for the download process
|
||||
def process_download(url):
|
||||
# wait for previous thread if not first in list
|
||||
current_thread = threading.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])
|
||||
|
||||
# clear file_cache
|
||||
ids.clear()
|
||||
titles.clear()
|
||||
urls.clear()
|
||||
|
||||
# get basic info for given url
|
||||
query = ydl.YoutubeDL({'quiet': True}).extract_info(url=url, download=False)
|
||||
parent = query['title']
|
||||
|
||||
# one of the three cases does not throw an exception
|
||||
# and therefore gets to download and return
|
||||
# kinda hacky but whatever
|
||||
|
||||
# if downloading playlist
|
||||
try:
|
||||
# this throws KeyError when downloading single file
|
||||
for video in query['entries']:
|
||||
if check_already_exists(video['id']): # todo: this shit aint tested
|
||||
query_db_threaded('INSERT INTO collection(playlist, video) VALUES (:folder, :id)',
|
||||
{'folder': parent + '\\', 'id': video['id']})
|
||||
continue
|
||||
|
||||
# this throws DownloadError when not downloading playlist
|
||||
ydl.YoutubeDL({'quiet': True}).extract_info('https://www.youtube.com/watch?v=' + video['id'],
|
||||
download=False)
|
||||
# add new entry to file_cache
|
||||
ids.append(video['id'])
|
||||
titles.append(video['title'])
|
||||
urls.append('https://www.youtube.com/watch?v=' + video['id'])
|
||||
|
||||
# start download
|
||||
download_all(parent)
|
||||
return
|
||||
|
||||
# when downloading: channel: DownloadError, single file: KeyError
|
||||
except (DownloadError, KeyError):
|
||||
pass
|
||||
|
||||
# if downloading channel
|
||||
try:
|
||||
# for every tab (videos/shorts)
|
||||
for tab in query['entries']:
|
||||
# for every video in their respective tabs
|
||||
for video in tab['entries']:
|
||||
if check_already_exists(video['id']):
|
||||
query_db_threaded('INSERT INTO collection(playlist, video) VALUES (:folder, :id)',
|
||||
{'folder': parent + '\\', 'id': video['id']})
|
||||
continue
|
||||
|
||||
# todo: there have been cases of duplicate urls or some with '/watch?v=@channel_name'
|
||||
# but no consistency has been observed
|
||||
# still works though so will not be checked for now
|
||||
ids.append(video['id'])
|
||||
titles.append(video['title'])
|
||||
urls.append('https://www.youtube.com/watch?v=' + video['id'])
|
||||
|
||||
# start download
|
||||
download_all(parent)
|
||||
return
|
||||
|
||||
# when downloading single file: KeyError
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# if downloading video
|
||||
try:
|
||||
# when downloading single files that already exist, there's no need for adjustments in db
|
||||
if not check_already_exists(query['id']):
|
||||
ids.append(query['id'])
|
||||
titles.append(query['title'])
|
||||
urls.append('https://www.youtube.com/watch?v=' + query['id'])
|
||||
|
||||
# start download
|
||||
download_all()
|
||||
return
|
||||
|
||||
# this is broad on purpose; there has been no exception thrown here _yet_
|
||||
except Exception as e:
|
||||
print('*** ' + str(e) + ' ***')
|
||||
|
||||
# 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
|
||||
# adding that entry to the site would be done -here- i guess
|
||||
|
||||
thread_queue.remove(current_thread)
|
||||
return
|
||||
|
||||
|
||||
# checks whether a video is already in db
|
||||
def check_already_exists(video_id) -> bool:
|
||||
res = query_db_threaded('SELECT name FROM video WHERE id = :id', {'id': video_id})
|
||||
if len(res) > 0:
|
||||
return True
|
||||
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'):
|
||||
# if no new files to download, there's nothing to do here
|
||||
if not len(urls) > 0: return
|
||||
|
||||
# new parent rowid
|
||||
rowid_new = None
|
||||
|
||||
# if a parent was specified
|
||||
if parent is not None:
|
||||
# insert new playlist into db and get the rowid of the new entry
|
||||
rowid_new = query_db_threaded('INSERT INTO playlist(name) VALUES (:name) RETURNING ROWID',
|
||||
{'name': parent})[0][0]
|
||||
|
||||
# set the base relative path for playlists
|
||||
relativePath = parent + '\\'
|
||||
|
||||
# does that subdirectory already exist?
|
||||
if os.path.exists(f'downloads\\{parent}'):
|
||||
subdirs = []
|
||||
for file in os.scandir(f'downloads\\{parent}'):
|
||||
if file.is_dir():
|
||||
subdirs.append(file)
|
||||
|
||||
# was that subdirectory not split into subdirectories already? (duplicate playlist names)
|
||||
if not len(subdirs) > 0:
|
||||
# get rowid of current playlist in 'downloads/parent/' directory
|
||||
parent_rowid = query_db_threaded('SELECT ROWID FROM playlist WHERE name = :playlist',
|
||||
{'playlist': parent})[0][0]
|
||||
|
||||
# update previous parents directory
|
||||
query_db_threaded('UPDATE playlist SET folder = :folder WHERE ROWID = :rowid',
|
||||
{'folder': relativePath + str(parent_rowid) + '\\', 'rowid': parent_rowid})
|
||||
|
||||
# update the folder entry in collection
|
||||
query_db_threaded('UPDATE collection SET playlist = :folder WHERE playlist = :folder_old',
|
||||
{'folder': relativePath + str(parent_rowid) + '\\', 'folder_old': relativePath})
|
||||
|
||||
# move all files into subdirectory 'downloads/parent/rowid'
|
||||
srcpath = downloads_path() + parent + '\\'
|
||||
dstpath = srcpath + str(parent_rowid) + '\\'
|
||||
for f in os.scandir(srcpath):
|
||||
os.renames(srcpath + f.name, dstpath + f.name)
|
||||
|
||||
# adjust path in db table video
|
||||
query_db_threaded('UPDATE video SET path = :new_path WHERE path = :old_path',
|
||||
{'new_path': relativePath + str(parent_rowid) + '\\', 'old_path': relativePath})
|
||||
|
||||
# append relative path
|
||||
relativePath += str(rowid_new) + '\\'
|
||||
|
||||
# set the relative path of playlist in recently added entry
|
||||
query_db_threaded('UPDATE playlist SET folder = :folder WHERE ROWID = :rowid',
|
||||
{'folder': relativePath, 'rowid': rowid_new})
|
||||
|
||||
# set path for new downloads
|
||||
location = downloads_path() + relativePath
|
||||
|
||||
# if that subdirectory does not already exist
|
||||
else:
|
||||
# set the relative path of playlist in recently added entry
|
||||
query_db_threaded('UPDATE playlist SET folder = :folder WHERE ROWID = :rowid',
|
||||
{'folder': relativePath, 'rowid': rowid_new})
|
||||
|
||||
# db_add needs to be passed none so the correct folder can be set in collection
|
||||
rowid_new = None
|
||||
|
||||
# set path for new downloads
|
||||
location = downloads_path() + relativePath
|
||||
|
||||
# if no parent was specified
|
||||
else:
|
||||
location = downloads_path()
|
||||
|
||||
# base download options for audio
|
||||
opts = {
|
||||
'quiet': False,
|
||||
'windowsfilenames': True,
|
||||
'outtmpl': location + '%(title)s.%(ext)s',
|
||||
'format': 'bestaudio/best',
|
||||
'postprocessors': [{
|
||||
'key': 'FFmpegExtractAudio',
|
||||
'preferredcodec': 'mp3',
|
||||
'preferredquality': 192
|
||||
}]
|
||||
}
|
||||
|
||||
# if videos are wanted, adjust the options
|
||||
if ext == 'mp4':
|
||||
opts.pop('format')
|
||||
opts.pop('postprocessors')
|
||||
|
||||
# try to download all new files
|
||||
try:
|
||||
ydl.YoutubeDL(opts).download(urls)
|
||||
except DownloadError:
|
||||
pass
|
||||
|
||||
# add downloaded files to db
|
||||
db_add(ext, rowid_new, parent)
|
||||
|
||||
return
|
||||
|
||||
|
||||
# a way to get the 'parent_root/downloads' directory; alternative for nonexistent global immutable
|
||||
# since app context does not exist where this is called, using app.config will not work
|
||||
def downloads_path() -> str:
|
||||
return os.path.dirname(os.path.abspath(__file__)) + '\\downloads\\'
|
||||
|
||||
|
||||
# updates zip in or creates new zip of given directory
|
||||
def zip_folder(full_rel_path):
|
||||
# get playlist name
|
||||
parent = full_rel_path.split('/')
|
||||
for folder in parent:
|
||||
if folder != '' and not folder.isdigit():
|
||||
parent = folder
|
||||
break
|
||||
|
||||
# get filename of existing zip else empty string
|
||||
existing_zip = directory_contains_zip(full_rel_path)
|
||||
|
||||
# get or generate filename
|
||||
if existing_zip:
|
||||
filename = existing_zip
|
||||
else:
|
||||
# generate filename without slashes as that might be a problem but that was not tested
|
||||
filename = (b64encode(os.urandom(4)).decode('utf-8')
|
||||
.replace('/', 'x')
|
||||
.replace('\\', 'y')
|
||||
.replace('=', 'z')
|
||||
.replace('+', 'a')
|
||||
) + '.zip'
|
||||
|
||||
# create archive
|
||||
zipfile.ZipFile(downloads_path() + full_rel_path + filename, 'w')
|
||||
|
||||
# Open the existing zip file in append mode
|
||||
with zipfile.ZipFile(downloads_path() + full_rel_path + filename, 'a') as existing_zip:
|
||||
file_list = existing_zip.namelist()
|
||||
file_list = [e[len(parent)+len(downloads_path())+1:] for e in file_list]
|
||||
|
||||
for entry in os.scandir(downloads_path() + full_rel_path):
|
||||
if entry.is_file() and not entry.name.endswith('.zip') and entry.name not in file_list:
|
||||
# Add the file to the zip, preserving the directory structure
|
||||
existing_zip.write(entry.path, arcname=parent + '\\' + entry.name)
|
||||
|
||||
return filename
|
||||
|
||||
|
||||
def directory_contains_zip(full_rel_path):
|
||||
for file in os.scandir(downloads_path() + full_rel_path):
|
||||
if file.name.endswith('.zip'):
|
||||
return file.name
|
||||
return ''
|
||||
4
file_cache.py
Normal file
4
file_cache.py
Normal file
@@ -0,0 +1,4 @@
|
||||
ids = []
|
||||
titles = []
|
||||
urls = []
|
||||
thread_queue = []
|
||||
@@ -1,5 +1,5 @@
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import URLField, SubmitField, Label
|
||||
from wtforms import URLField, SubmitField
|
||||
from wtforms.validators import URL, DataRequired
|
||||
|
||||
|
||||
|
||||
146
frontend.py
146
frontend.py
@@ -1,14 +1,14 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
import sqlite3
|
||||
import yt_dlp as ydl
|
||||
from threading import Thread
|
||||
|
||||
from flask import Blueprint, render_template, send_from_directory, current_app, g
|
||||
from flask import Blueprint, request, render_template, flash, send_from_directory, current_app
|
||||
from flask_nav3 import Nav
|
||||
from flask_nav3.elements import Navbar, View
|
||||
|
||||
from forms.download import DownloadForm
|
||||
from backend import query_db, process_download, zip_folder, downloads_path
|
||||
from file_cache import *
|
||||
|
||||
frontend = Blueprint('frontend', __name__)
|
||||
|
||||
@@ -22,6 +22,9 @@ nav.register_element('frontend_top', Navbar(
|
||||
)
|
||||
|
||||
|
||||
# there's basically nothing on index
|
||||
# todo: a nice homepage or even a login could be nice
|
||||
# those that have a need for it and are able to make it secure, are invited to open a merge request
|
||||
@frontend.route('/', methods=['GET'])
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
@@ -29,61 +32,51 @@ def index():
|
||||
|
||||
@frontend.route('/downloader', methods=['GET', 'POST'])
|
||||
def downloader():
|
||||
# get form data
|
||||
form = DownloadForm()
|
||||
|
||||
# get url out of form
|
||||
url = str(form.url.data)
|
||||
|
||||
# so error message does not get displayed when url is empty
|
||||
# validate_on_submit requires len(str) > 0 in DownloadForm
|
||||
ytLink = True
|
||||
titles = []
|
||||
urls = []
|
||||
# check if valid link
|
||||
# this tool should technically work with other platforms but that is not tested
|
||||
# since KeyErrors are to be expected in backend.process_download(url), it's blocked here
|
||||
# you are invited to test and adjust the code for other platforms and open a merge request
|
||||
valid_link = True if 'youtube.com' in url or 'youtu.be' in url else False
|
||||
|
||||
if form.validate_on_submit():
|
||||
ytLink = True if 'youtube.com' in url else False
|
||||
if ytLink:
|
||||
query = ydl.YoutubeDL({'quiet': True}).extract_info(url=url, download=False)
|
||||
# if there has been a problem with the form (empty or error) or the link is not valid
|
||||
if not form.validate_on_submit() or not valid_link:
|
||||
valid_link = True if url == 'None' else False # if url is empty, don't show error
|
||||
return render_template('downloader.html', form=form, ytLink=valid_link, titles=titles, urls=urls,
|
||||
amount=len(titles))
|
||||
|
||||
# if downloading playlist
|
||||
try:
|
||||
for video in query['entries']:
|
||||
ydl.YoutubeDL({'quiet': True}).extract_info('https://www.youtube.com/watch?v=' + video['id'], download=False)
|
||||
titles.append(video['title'])
|
||||
urls.append('https://www.youtube.com/watch?v=' + video['id'])
|
||||
return render_template('downloader.html', form=form, ytLink=ytLink, titles=titles, urls=urls,
|
||||
amount=len(titles))
|
||||
except:
|
||||
pass
|
||||
# download and processing is happening in background / another thread
|
||||
t = Thread(target=process_download, args=(url,))
|
||||
thread_queue.append(t)
|
||||
t.start()
|
||||
|
||||
# if downloading channel
|
||||
try:
|
||||
for tab in query['entries']:
|
||||
for video in tab['entries']:
|
||||
titles.append(video['title'])
|
||||
urls.append('https://www.youtube.com/watch?v=' + video['id'])
|
||||
return render_template('downloader.html', form=form, ytLink=ytLink, titles=titles, urls=urls,
|
||||
amount=len(titles))
|
||||
except:
|
||||
pass
|
||||
|
||||
# if downloading video
|
||||
try:
|
||||
titles.append(query['title'])
|
||||
urls.append('https://www.youtube.com/watch?v=' + query['id'])
|
||||
return render_template('downloader.html', form=form, ytLink=ytLink, titles=titles, urls=urls,
|
||||
amount=len(titles))
|
||||
except:
|
||||
pass
|
||||
|
||||
return render_template('downloader.html', form=form, ytLink=ytLink, titles=titles, urls=urls, amount=len(titles))
|
||||
# show download start confirmation
|
||||
flash('Download started and will continue in background.')
|
||||
return render_template('new-downloads.html', titles=titles, urls=urls, amount=len(titles))
|
||||
|
||||
|
||||
@frontend.route('/download/<path:file>', methods=['GET'])
|
||||
def download(file):
|
||||
return send_from_directory(
|
||||
os.path.join(current_app.root_path, 'downloads/'),
|
||||
file
|
||||
)
|
||||
# downloads a single file
|
||||
@frontend.route('/download/<path:file_path>', methods=['GET'])
|
||||
def download(file_path):
|
||||
# if the path does not end with a slash, a single file is requested
|
||||
if '.' in file_path:
|
||||
return send_from_directory(
|
||||
downloads_path(),
|
||||
file_path
|
||||
)
|
||||
|
||||
# else a directory is requested
|
||||
else:
|
||||
# zip and send
|
||||
return send_from_directory(
|
||||
downloads_path(),
|
||||
file_path + zip_folder(file_path)
|
||||
)
|
||||
|
||||
|
||||
@frontend.route('/update', methods=['GET', 'POST'])
|
||||
@@ -93,47 +86,16 @@ def updater():
|
||||
|
||||
@frontend.route('/library', methods=['GET'])
|
||||
def library():
|
||||
videos = query_db("SELECT name FROM video")
|
||||
playlists = query_db("SELECT name FROM playlist")
|
||||
return render_template('library.html', videos=videos, playlists=playlists)
|
||||
videos = query_db("SELECT name, ext, path FROM video")
|
||||
playlists = query_db("SELECT name, ROWID FROM playlist")
|
||||
return render_template('library.html', videos=videos, playlists=playlists, amount=len(playlists))
|
||||
|
||||
|
||||
@frontend.route("/collection")
|
||||
def collection():
|
||||
query = query_db("""
|
||||
SELECT video.name FROM video
|
||||
INNER JOIN collection ON collection.path = video.filename
|
||||
INNER JOIN playlist ON playlist.ROWID = collection.playlist
|
||||
WHERE video.name IS ?;
|
||||
""", ("",))
|
||||
return render_template('collection.html', query=query)
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def query_db(query, args=(), one=False):
|
||||
cur = get_db().execute(query, args)
|
||||
res = cur.fetchall()
|
||||
cur.close()
|
||||
return (res[0] if res else None) if one else res
|
||||
|
||||
|
||||
def db_add_collection(info):
|
||||
return
|
||||
|
||||
|
||||
def db_add_single(info):
|
||||
return
|
||||
|
||||
|
||||
async def download_all(info):
|
||||
return
|
||||
|
||||
|
||||
# todo: cache results from extract_info for /download and don't fetch again. update of existing downloads only over /update
|
||||
@frontend.route('/library-playlist', methods=['GET'])
|
||||
def library_playlist():
|
||||
playlist = request.args.get('playlist', None)
|
||||
videos = query_db('SELECT video.name, video.ext, video.path FROM video LEFT JOIN collection ON video.id = '
|
||||
'collection.video LEFT JOIN playlist ON collection.playlist=playlist.folder WHERE '
|
||||
'playlist.ROWID = :playlist',
|
||||
{'playlist': playlist})
|
||||
return render_template('collection.html', videos=videos)
|
||||
|
||||
@@ -2,6 +2,6 @@ flask
|
||||
flask_sqlalchemy
|
||||
flask_bootstrap
|
||||
flask_nav3
|
||||
flask_login
|
||||
flask_wtf
|
||||
wtforms
|
||||
yt_dlp
|
||||
|
||||
51
schema.sql
51
schema.sql
@@ -1,13 +1,44 @@
|
||||
/*
|
||||
- unique youtube id / watch key identifies the song
|
||||
- id is nullable for todo: ingest from local dev
|
||||
- name is title of video
|
||||
- ext is the file extension / type
|
||||
- path is relative to 'project_root/downloads/'
|
||||
*/
|
||||
CREATE TABLE IF NOT EXISTS video (
|
||||
filename TEXT PRIMARY KEY NOT NULL,
|
||||
name TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS playlist (
|
||||
name TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS collection (
|
||||
playlist INTEGER NOT NULL,
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
ext TEXT NOT NULL,
|
||||
path TEXT NOT NULL
|
||||
);
|
||||
|
||||
/*
|
||||
- folder is relative to 'downloads/'
|
||||
- name is title of channel / playlist
|
||||
- if playlist name is new, all files
|
||||
go into 'downloads/name/'
|
||||
- if playlist name and therefore folder
|
||||
already exists, 'downloads/name/' dir
|
||||
gets subdirectories named after their
|
||||
respective ROWID
|
||||
example for folder:
|
||||
- 'playlist_name/'
|
||||
- 'playlist_name/playlist_ROWID/'
|
||||
*/
|
||||
CREATE TABLE IF NOT EXISTS playlist (
|
||||
folder TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL
|
||||
);
|
||||
|
||||
/*
|
||||
- playlist equals folder
|
||||
- video equals id
|
||||
- simple n-m mapping
|
||||
(playlist contains multiple songs)
|
||||
(song can be in multiple playlists)
|
||||
*/
|
||||
CREATE TABLE IF NOT EXISTS collection (
|
||||
playlist TEXT NOT NULL,
|
||||
video TEXT NOT NULL
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{%- extends "bootstrap/base.html" %}
|
||||
{% import "bootstrap/fixes.html" as fixes %}
|
||||
{% import "bootstrap/utils.html" as utils %}
|
||||
|
||||
{% block title %}
|
||||
yt-dls
|
||||
@@ -10,14 +11,18 @@
|
||||
{{ fixes.ie8() }}
|
||||
{% endblock %}
|
||||
|
||||
{% block styles %}
|
||||
{{ super() }}
|
||||
{# Commented out cause don't exist
|
||||
<link rel="stylesheet" type="text/css"
|
||||
href="{{url_for('static', filename='example.css')}}">
|
||||
#}
|
||||
{% endblock %}
|
||||
|
||||
{% block navbar %}
|
||||
{{ nav.frontend_top.render() }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{%- with messages = get_flashed_messages(with_categories=True) %}
|
||||
{%- if messages %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{{utils.flashed_messages(messages)}}
|
||||
</div>
|
||||
</div>
|
||||
{%- endif %}
|
||||
{%- endwith %}
|
||||
{% endblock %}
|
||||
@@ -1,28 +1,26 @@
|
||||
{%- extends "base.html" %}
|
||||
|
||||
{% import "bootstrap/utils.html" as utils %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
{%- with messages = get_flashed_messages(with_categories=True) %}
|
||||
{%- if messages %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{{utils.flashed_messages(messages)}}
|
||||
</div>
|
||||
</div>
|
||||
{%- endif %}
|
||||
{%- endwith %}
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<table id="library" style="width:80%">
|
||||
{% for name in query %}
|
||||
{{ super() }}
|
||||
<div class="container" style="width: 50%">
|
||||
<table id="videos" class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>{{ name }}</td>
|
||||
<td>Download</td>
|
||||
<th scope="col" class="text-center">Song</th>
|
||||
<th scope="col" class="text-center">Download</th>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for video in videos %}
|
||||
<tr>
|
||||
<td class="text-center">{{ video['name'] }}</td>
|
||||
<td class="text-center"><a href="/download/{{ video['path'] + video['name'] + video['ext'] }}" download>Link</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<form action="/download/{{ videos[0]['path'] }}">
|
||||
<input type="submit" class="btn pull-right" value="Download all"/>
|
||||
</form>
|
||||
</div>
|
||||
{%- endblock %}
|
||||
@@ -1,39 +1,28 @@
|
||||
{%- extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
{{ super() }}
|
||||
<div class="container">
|
||||
<form method="POST">
|
||||
{{ form.csrf_token }}
|
||||
{{ form.url.label }} <br>
|
||||
{{ form.url }}
|
||||
{{ form.submit }}
|
||||
</form>
|
||||
<div class="card" style="width: fit-content">
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<form method="POST">
|
||||
{{ form.csrf_token }}
|
||||
{{ form.url.label }} <br>
|
||||
{{ form.url }}
|
||||
{{ form.submit }}
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{% if form.errors %}
|
||||
{{ form.errors['url'][0][:-1] + ', try again.' }}
|
||||
{% endif %}
|
||||
{% if ytLink == False %}
|
||||
|
||||
{% if not ytLink %}
|
||||
Please enter a full, valid YouTube URL.
|
||||
{% endif %}
|
||||
|
||||
{% if titles %}
|
||||
<div class="container">
|
||||
<table id="videos" class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="text-center">Title</th>
|
||||
<th scope="col" class="text-center">URL</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for i in range(amount) %}
|
||||
<tr>
|
||||
<td class="text-center">{{ titles[i] }}</td>
|
||||
<td class="text-center"> <a href="{{ urls[i] }}" target="_blank">Link</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{%- endblock %}
|
||||
@@ -3,19 +3,8 @@
|
||||
{% import "bootstrap/utils.html" as utils %}
|
||||
|
||||
{% block content %}
|
||||
{{ super() }}
|
||||
<div class="container">
|
||||
|
||||
{%- with messages = get_flashed_messages(with_categories=True) %}
|
||||
{%- if messages %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{{utils.flashed_messages(messages)}}
|
||||
</div>
|
||||
</div>
|
||||
{%- endif %}
|
||||
{%- endwith %}
|
||||
|
||||
<a href="http://pythonhosted.org/Flask-Bootstrap">Documentation</a>. </p>
|
||||
|
||||
</div>
|
||||
{%- endblock %}
|
||||
@@ -1,47 +1,58 @@
|
||||
{%- extends "base.html" %}
|
||||
|
||||
{% import "bootstrap/utils.html" as utils %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<table id="videos" class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="text-center">Song</th>
|
||||
<th scope="col" class="text-center">Download</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for video in videos %}
|
||||
<tr>
|
||||
<td class="text-center">{{ video['name'] }}</td>
|
||||
<td class="text-center">Link</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{{ super() }}
|
||||
<div class="container">
|
||||
|
||||
<div class="card" style="width: 100%">
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<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 video in videos %}
|
||||
<tr>
|
||||
<td class="text-center">{{ video['name'] }}</td>
|
||||
<td class="text-center"><a href="/download/{{ video['path'] + video['name'] + video['ext'] }}" download>Link</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<br><br>
|
||||
|
||||
|
||||
<div class="col-md-1"></div>
|
||||
|
||||
<div class="col-md-2">
|
||||
<table id="playlists" class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="text-center">Playlists</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for playlist in playlists %}
|
||||
{% if playlists %}
|
||||
<div class="col-md-2">
|
||||
<table id="playlists" class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="text-center">{{ playlist['name'] }}</td>
|
||||
<th scope="col" class="text-center">Playlists</th>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for playlist in playlists %}
|
||||
<tr>
|
||||
<td class="text-center"> <a href="/library-playlist?playlist={{ playlist['ROWID'] }}">{{ playlist['name'] }}</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{%- endblock %}
|
||||
31
templates/new-downloads.html
Normal file
31
templates/new-downloads.html
Normal file
@@ -0,0 +1,31 @@
|
||||
{%- extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
{{ super() }}
|
||||
<div class="container">
|
||||
|
||||
{% if titles %}
|
||||
<div class="container">
|
||||
<table id="videos" class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="text-center">New Title(s)</th>
|
||||
<th scope="col" class="text-center">URL</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for i in range(amount) %}
|
||||
<tr>
|
||||
<td class="text-center">{{ titles[i] }}</td>
|
||||
<td class="text-center"> <a href="{{ urls[i] }}" target="_blank">Link</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<form target="/start-download"
|
||||
<input type="submit"
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{%- endblock %}
|
||||
@@ -1,8 +1,7 @@
|
||||
{%- extends "base.html" %}
|
||||
|
||||
{% import "bootstrap/utils.html" as utils %}
|
||||
|
||||
{% block content %}
|
||||
{{ super() }}
|
||||
<div class="container">
|
||||
{%- with messages = get_flashed_messages(with_categories=True) %}
|
||||
{%- if messages %}
|
||||
|
||||
Reference in New Issue
Block a user