Playlist deletion is now possible. Titles that are in multiple playlists get saved from doom. New utils out of necessity.

This commit is contained in:
Maximilian Wagner
2023-08-05 00:35:46 +02:00
parent 5191c15692
commit a72c9eac39
6 changed files with 204 additions and 60 deletions

2
app.py
View File

@@ -2,7 +2,7 @@ from flask import Flask, current_app, g
from flask_wtf.csrf import CSRFProtect
from frontend import frontend
from backend import get_db
from db_tools import get_db
from os import urandom

View File

@@ -8,9 +8,20 @@ from urllib.request import urlopen
from urllib.error import URLError
from base64 import b64encode
from threading import Thread
from shutil import rmtree
from db_tools import *
from db_tools import (
add_new_video_to_collection,
query_db,
query_db_threaded,
db_add_via_update,
db_add_via_download,
update_playlist_folder_by_rowid,
remove_video,
remove_playlist
)
from file_cache import *
from utils import downloads_path, dissect_file_name
# adds thread to queue and starts it
@@ -79,6 +90,10 @@ def process_download(url, ext, parent, query, current_thread):
# this throws DownloadError when not downloading playlist
ydl.YoutubeDL({'quiet': True}).extract_info('https://www.youtube.com/watch?v=' + video['id'],
download=False)
# replace url with name of playlist
queued_downloads[0][0] = parent
# add new entry to file_cache
ids.append(video['id'])
titles.append(video['title'])
@@ -103,6 +118,9 @@ def process_download(url, ext, parent, query, current_thread):
add_new_video_to_collection(parent, video['id'])
continue
# replace url with name of channel
queued_downloads[0][0] = parent
# 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
@@ -127,6 +145,9 @@ def process_download(url, ext, parent, query, current_thread):
titles.append(query['title'])
urls.append('https://www.youtube.com/watch?v=' + query['id'])
# replace url with name of video
queued_downloads[0][0] = query['title']
# start download
download_all(url, ext=ext)
@@ -134,15 +155,15 @@ def process_download(url, ext, parent, query, current_thread):
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
thread_queue.remove(current_thread)
return
# this is the 'controller' for the update process
def process_update(parent, query, current_thread):
# replace url with name of playlist
queued_downloads[0][0] = parent
# if updating playlist
try:
# this throws KeyError when downloading single file
@@ -154,6 +175,7 @@ def process_update(parent, query, current_thread):
# 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'])
@@ -216,7 +238,8 @@ def download_all(url, ext, parent=None):
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, url) VALUES (:name, :url) RETURNING ROWID',
{'name': parent, 'url': url})[0][0]
{'name': parent, 'url': url},
True)[0]
# set the base relative path for playlists
relativePath = parent + '\\'
@@ -232,7 +255,8 @@ def download_all(url, ext, parent=None):
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]
{'playlist': parent},
True)[0]
# update previous parents directory
query_db_threaded('UPDATE playlist SET folder = :folder WHERE ROWID = :rowid',
@@ -327,12 +351,6 @@ def yt_download(location, ext='mp3'):
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) -> tuple[str, str]:
# get playlist name
@@ -373,6 +391,7 @@ def zip_folder(full_rel_path) -> tuple[str, str]:
return full_rel_path, filename
# adds all files that are in the playlist but not in its folder to the zip
def zip_folder_not_in_directory(zip_full_rel_path):
video_not_in_directory = query_db_threaded('SELECT path, name, ext FROM video '
'INNER JOIN collection ON video.id = collection.video '
@@ -395,6 +414,7 @@ def zip_folder_not_in_directory(zip_full_rel_path):
return
# returns name of first zip found or empty string
def directory_contains_zip(full_rel_path):
for file in os.scandir(downloads_path() + full_rel_path):
if file.name.endswith('.zip'):
@@ -402,9 +422,39 @@ def directory_contains_zip(full_rel_path):
return ''
# checks if yt or any given target can be reached
def internet_available(target='http://www.youtube.com'):
try:
urlopen(target)
return True
except URLError:
return False
# does what it says; does not need thread of its own since it's reasonably fast
def delete_file_or_playlist(file_name):
# deleting single download is simple enough
if '.' in file_name:
remove_video(file_name)
os.remove(downloads_path() + file_name)
return
# get folder from file_name
folder, _, _ = dissect_file_name(file_name)
# remove playlist from db and get videos that are also in other playlists
# todo: this needs to be tested at some point
videos_to_move = remove_playlist(folder)
# move all videos rescued from being deleted
if videos_to_move:
for video in videos_to_move:
os.rename(
downloads_path() + video[0] + video[1] + video[2],
downloads_path() + video[1] + video[2]
)
# delete the folder in which the playlist was stored
rmtree(downloads_path() + folder)
return

View File

@@ -1,6 +1,7 @@
from flask import g
import sqlite3
from file_cache import ids, titles
from utils import dissect_file_name
# fetches db from app context
@@ -92,3 +93,55 @@ def update_playlist_folder_by_rowid(folder, rowid):
query_db_threaded('UPDATE playlist SET folder = :folder WHERE ROWID = :rowid',
{'folder': folder, 'rowid': rowid})
return
# removes a single video
def remove_video(file_name: str) -> bool:
folder, name, ext = dissect_file_name(file_name)
rowid = query_db('DELETE FROM video '
'WHERE name = :name AND path = :path AND ext = :ext '
'RETURNING ROWID',
{'name': name, 'path': folder, 'ext': ext},
True)
return True if rowid else False
# removes playlist and all contained videos from db
def remove_playlist(folder):
rescued = rescue_videos(folder)
query_db_threaded('DELETE FROM playlist '
'WHERE folder = :folder',
{'folder': folder})
query_db_threaded('DELETE FROM collection '
'WHERE playlist = :folder ',
{'folder': folder})
query_db('DELETE FROM video '
'WHERE path = :path ',
{'path': folder})
return rescued
# removes videos from the playlist to delete if they are also in other playlists
# and sets path to download root
def rescue_videos(folder):
videos = query_db('SELECT id, path, name, ext FROM collection '
'LEFT JOIN video ON video = id '
'WHERE NOT path = playlist AND path = :path',
{'path': folder})
if videos:
for video in videos:
query_db('UPDATE video SET path = \'\' '
'WHERE id = :id',
{'id': video[0]})
query_db('DELETE FROM collection '
'WHERE video = :id AND playlist = :playlist',
{'id': video[0], 'playlist': folder})
return [(x['path'], x['name'], x['ext']) for x in videos] if videos else None

View File

@@ -4,9 +4,16 @@ from __future__ import unicode_literals
from flask import Blueprint, request, render_template, flash, send_from_directory, send_file
from forms.download import DownloadForm
from backend import zip_folder, zip_folder_not_in_directory, downloads_path, enqueue_download, internet_available
from backend import (
zip_folder,
zip_folder_not_in_directory,
enqueue_download,
internet_available,
delete_file_or_playlist
)
from db_tools import query_db
from file_cache import *
from utils import downloads_path, dissect_file_name
frontend = Blueprint('frontend', __name__)
@@ -51,52 +58,6 @@ def downloader():
return render_template('flash-message.html')
# 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:
split_path = file_path.split('/')
file_folder = ''.join([x if x not in split_path[-1] else '' for x in split_path])
video = query_db('SELECT path, name, ext FROM video WHERE name = :name AND path = :path',
{
'name': split_path[-1].split('.')[0],
'path': file_folder + '\\' if file_folder else ''
},
True)
return send_from_directory(
downloads_path() + video['path'],
video['name'] + video['ext']
)
# else a directory is requested
else:
zip_path, zip_name = zip_folder(file_path)
print(zip_path, zip_name)
zip_folder_not_in_directory(zip_path + zip_name)
# zip and send
return send_from_directory(
downloads_path() + zip_path,
zip_name
)
@frontend.route('/update/<int:url_rowid>', methods=['GET'])
def update(url_rowid):
url = query_db('SELECT url FROM playlist WHERE ROWID = :url_rowid',
{'url_rowid': url_rowid})[0][0]
# kick off download process
enqueue_download(url, update=True)
# show download start confirmation
flash('Update enqueued and will finish in background.', 'primary')
return render_template('flash-message.html', titles=titles, urls=urls, amount=len(urls))
@frontend.route('/library', methods=['GET'])
def library():
videos = query_db("SELECT name, ext, path FROM video "
@@ -105,6 +66,8 @@ def library():
if not playlists and not videos:
flash('Library ist currently empty. Try downloading something!', 'primary')
# todo: searching your library for a song would be nice
return render_template('library.html', videos=videos, playlists=playlists, amount=len(playlists))
@@ -127,6 +90,62 @@ def library_playlist():
return render_template('collection.html', videos=videos, folder=folder)
# sends file or playlist to client
@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:
path, name, _ = dissect_file_name(file_path)
video = query_db('SELECT path, name, ext FROM video WHERE name = :name AND path = :path',
{'name': name, 'path': path},
True)
return send_from_directory(
downloads_path() + video['path'],
video['name'] + video['ext']
)
# else a directory is requested
else:
zip_path, zip_name = zip_folder(file_path)
print(zip_path, zip_name)
zip_folder_not_in_directory(zip_path + zip_name)
# zip and send
return send_from_directory(
downloads_path() + zip_path,
zip_name
)
@frontend.route('/delete/<path:file_name>', methods=['GET'])
def delete(file_name):
delete_file_or_playlist(file_name)
if '.' in file_name:
flash('File has been deleted.', 'primary')
else:
flash('Playlist has been deleted.', 'primary')
return render_template('flash-message.html')
@frontend.route('/update/<int:url_rowid>', methods=['GET'])
def update(url_rowid):
url = query_db('SELECT url FROM playlist WHERE ROWID = :url_rowid',
{'url_rowid': url_rowid})[0][0]
# kick off download process
enqueue_download(url, update=True)
# show download start confirmation
flash('Update enqueued and will finish in background.', 'primary')
return render_template('flash-message.html', titles=titles, urls=urls, amount=len(urls))
# player as well as serve are placeholders for now
# todo: add functionality to library
@frontend.route('/player', methods=['GET'])
def player():
return render_template('video-player.html')

View File

@@ -25,6 +25,9 @@
</div>
</div>
<div class="container" style="padding: 1.5%">
<form action="/delete/{{ folder }}">
<input type="submit" class="btn btn-danger float-start" value="Delete playlist">
</form>
<form action="/download/{{ folder }}">
<input type="submit" class="btn btn-primary float-end" value="Download all"/>
</form>

19
utils.py Normal file
View File

@@ -0,0 +1,19 @@
import os.path as path
# 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 path.dirname(path.abspath(__file__)) + '\\downloads\\'
# dissects a given full path to a file into its components
def dissect_file_name(file_name: str) -> tuple[str, str, str]:
split_path = file_name.replace('/', '\\').split('\\')
full_name = split_path[-1].split('.')
folder = ''.join([x + '\\' if x and '.' not in x else '' for x in split_path])
name = full_name[0] if full_name[0] else ''
ext = '.' + full_name[1] if full_name[0] else ''
return folder, name, ext