-
+
diff --git a/metatube/youtube.py b/metatube/youtube.py
deleted file mode 100644
index 66b600a7..00000000
--- a/metatube/youtube.py
+++ /dev/null
@@ -1,255 +0,0 @@
-import yt_dlp, json, os
-from yt_dlp.postprocessor.ffmpeg import FFmpegPostProcessorError
-from yt_dlp.postprocessor.metadataparser import MetadataParserPP
-from youtubesearchpython import VideosSearch
-from threading import Thread
-from urllib.error import URLError
-from yt_dlp.utils import ExtractorError, DownloadError, PostProcessingError
-from metatube import sockets, logger
-from metatube.sponsorblock import segments as findsegments
-from jinja2 import Environment, PackageLoader, select_autoescape
-import asyncio
-from functools import partial
-from queue import LifoQueue, Empty
-from time import sleep
-
-
-class YouTube:
- @staticmethod
- def is_supported(url):
- extractors = yt_dlp.extractor.gen_extractors()
- for e in extractors:
- if e.suitable(url) and e.IE_NAME == 'youtube':
- return True
- return False
-
- @staticmethod
- def fetch_url(url, verbose):
- if YouTube.is_supported(url):
- ytdl_options = {'logger': logger, 'verbose': verbose}
- with yt_dlp.YoutubeDL(ytdl_options) as ytdl:
- try:
- info = ytdl.extract_info(url, download=False)
- return info
- except Exception as e:
- return str(e)
- else:
- raise ValueError("Invalid URL!")
-
- @staticmethod
- def verifytemplate(template, info_dict, verbose):
- ytdl_options = {'logger': logger, 'verbose': verbose}
- with yt_dlp.YoutubeDL(ytdl_options) as ytdl:
- try:
- filename = ytdl.evaluate_outtmpl(template, info_dict)
- return filename
- except Exception as e:
- return str(e)
-
- @staticmethod
- def search(query: str):
- logger.info('Searching YouTube for \'%s\'', query)
- search = VideosSearch(query)
- result = search.result()
- sockets.youtubesearch(result)
-
- @staticmethod
- async def download(url: list, queue: LifoQueue, ytdl_options: dict):
- download_hook_partial = partial(YouTube.download_hook, queue)
- ytdl_options['progress_hooks'] = [download_hook_partial]
- ytdl_options['postprocessor_hooks'] = [YouTube.postprocessor_hook]
- with yt_dlp.YoutubeDL(ytdl_options) as ytdl:
- try:
- return ytdl.download(url)
- except KeyError as e:
- logger.error('%s key did not exist', str(e))
- sockets.downloaderrors({'status': 'error', 'message': 'The output template was incorrect. Check logs for more info.'})
- return None
- except ExtractorError as e:
- logger.error('Extractor error: %s', str(e))
- sockets.downloaderrors({'status': 'error', 'message': 'An extractor error has occured. Check logs for more info.'})
- return None
- except FFmpegPostProcessorError as e:
- logger.error('FFmpegPostProcessor error: %s', str(e))
- sockets.downloaderrors({'status': 'error', 'message': 'An processing error involving FFmpeg has occured. Check logs for more info.'})
- return None
- except PostProcessingError as e:
- logger.error('Postprocessor error: %s', str(e))
- sockets.downloaderrors({'status': 'error', 'message': 'A processing error has occured. Check logs for more info.'})
- return None
- except DownloadError as e:
- logger.error('Downloading error: %s', str(e))
- sockets.downloaderrors({'status': 'error', 'message': 'A downloading error has occured. Check logs for more info.'})
- return None
- except URLError as e:
- logger.error('Network connection error: %s', str(e))
- sockets.downloaderrors({'status': 'error', 'message': 'A network error occured. Check logs for more info.'})
- return None
- except Exception as e:
- logger.exception('Error during downloading video: %s', str(e))
- sockets.downloaderrors({'status': 'error', 'message': 'Something has gone wrong. Check logs for more info'})
- return None
-
- @staticmethod
- def download_hook(queue: LifoQueue, d):
- queue.put(d)
-
- @staticmethod
- def postprocessor_hook(d):
- if d['status'] == 'processing':
- sockets.postprocessing(d['postprocessor'])
- elif d['status'] == 'finished':
- sockets.finished_postprocessor(d['postprocessor'], d['info_dict']['filepath'])
-
- @staticmethod
- def get_options(ext, output_folder, type, output_format, bitrate, skipfragments, proxy_data, ffmpeg, hw_transcoding, vaapi_device, width, height, verbose):
- proxy = json.loads(proxy_data)
- filepath = os.path.join(output_folder, output_format)
- segments = json.loads(skipfragments)
- postprocessors = []
- postprocessor_args = {}
- proxy_string = ""
- ext = "m4a" if "m4a" in ext else ext
- '''
- Audio:
- If an audio type has been selected, first try to look for a format with the selected extension
- If no audio format with the selected extension has been found, just look for the best audio format
- and automatically convert it to the selected extension anyway
- Video:
- Exactly the same for videos
- '''
- format = f'ba[ext={ext}]/ba' if type == 'Audio' else f'b[ext={ext}]/ba+bv[ext={ext}]/b/ba+bv'
-
- # choose whether to use the FFmpegExtractAudio post processor or the FFmpegVideoConverter one
- if type == 'Audio':
- postprocessors.append({
- "key": "FFmpegExtractAudio",
- "preferredcodec": ext,
- "preferredquality": bitrate
- })
- elif type == 'Video':
- postprocessors.append({
- "key": "FFmpegVideoConvertor",
- "preferedformat": ext
- })
- postprocessor_args['videoconvertor'] = []
- if bitrate != 'best':
- postprocessor_args["videoconvertor"] = ['-b:a', str(bitrate) + "k"]
-
- if height != 'best' and width != 'best':
- postprocessor_args["videoconvertor"][:0] = ['-vf', 'scale=' + str(width) + ':' + str(height)]
-
- # If hardware transcoding isn't None, add a hardware transcoding thingy to the FFmpeg arguments
- if hw_transcoding != 'None':
- if "videoconvertor" not in postprocessor_args:
- postprocessor_args["videoconvertor"] = []
- if hw_transcoding == 'nvenc':
- postprocessor_args["videoconvertor"].extend(['-c:v', 'h264_nvenc'])
- elif hw_transcoding == 'qsv':
- postprocessor_args["videoconvertor"].extend(['-c:v', 'h264_qsv'])
- elif hw_transcoding == 'videotoolbox':
- postprocessor_args["videoconvertor"].extend(['-c:v', 'h264_videotoolbox'])
- elif 'vaapi' in hw_transcoding:
- postprocessor_args["videoconvertor"].extend(['-vaapi_device', vaapi_device, '-c:v', 'h264_vaapi'])
- elif hw_transcoding == 'amd':
- postprocessor_args["videoconvertor"].extend(['-c:v', 'h264_amf'])
- elif hw_transcoding == 'omx':
- postprocessor_args["videoconvertor"].extend(['-c:v', 'h264_omx'])
-
- # If segments have been submitted by the user to exclude, add a ModifyChapters key and add ranges
- if len(segments) > 0:
- ranges = []
- for segment in segments:
- if len(segment["start"]) < 1 or len(segment["end"]) < 1:
- sockets.searchvideo('Enter all fragment fields!')
- return False
- else:
- ranges.append((int(segment["start"]), int(segment["end"])))
- postprocessors.append({
- 'key': 'ModifyChapters',
- 'remove_ranges': ranges
- })
-
- '''
- --parse-metadata example in CLI:
- yt-dlp orJSJGHjBLI -x --audio-format mp3 --add-metadata -o "%(track,title)s - %(artist)s.%(ext)s" --parse-metadata " Bad Habits: %(title)s" --parse-metadata "Ed Sheeran:%(artist)s"
- '''
- # postprocessors.append({
- # 'actions': [
- # (MetadataParserPP.interpretter, " " + metadata['title'], ' %(title)s'),
- # (MetadataParserPP.interpretter, metadata['album'], '%(album)s'),
- # (MetadataParserPP.interpretter, ';'.join(json.loads(metadata['album_artists'])), '%(album_artist)s'),
- # (MetadataParserPP.interpretter, metadata['album_tracknr'], '%(track_number)s'),
- # ],
- # 'key': 'MetadataParser',
- # 'when': 'pre_process'
- # })
-
- ytdl_options = {
- 'format': format,
- 'merge_output_format': ext,
- 'postprocessors': postprocessors,
- 'postprocessor_args': postprocessor_args,
- 'ffmpeg_location': ffmpeg,
- 'logger': logger,
- 'outtmpl': filepath,
- 'noplaylist': True,
- 'verbose': verbose
- }
-
- # Add proxy if proxy is enabled
- if proxy['proxy_type'] != 'None':
- proxy_string = proxy["proxy_type"].lower().strip() + "://"
- if len(proxy["proxy_username"]) > 0 and len(proxy["proxy_username"]) > 0:
- proxy_string += proxy["proxy_username"] + ":" + proxy["proxy_password"] + "@" + proxy["proxy_address"].strip() + ":" + proxy["proxy_port"].strip()
- else:
- proxy_string += proxy["proxy_address"].strip() + ":" + proxy["proxy_port"].strip()
- ytdl_options["proxy"] = proxy_string
- return ytdl_options
-
- @staticmethod
- def start_download(url, ytdl_options):
- loop = asyncio.new_event_loop()
- asyncio.set_event_loop(loop)
- queue = LifoQueue()
- coros = [YouTube.download(url, queue, ytdl_options)]
- future = asyncio.gather(*coros)
- thread = Thread(target=YouTube.loop_in_thread, args=[loop, future])
- thread.start()
- # While the future isn't finished yet continue
- while not future.done():
- try:
- # Get the latest status update from the que and print it
- d = queue.get_nowait()
- if d['status'] == 'downloading':
- if "total_bytes_estimate" in d:
- downloaded_bytes = d['downloaded_bytes'] or 'Unknown'
- total_bytes = d['total_bytes_estimate'] or d['total_bytes'] or 'Unknown'
- sockets.downloadprogress(downloaded_bytes, total_bytes)
- elif d['status'] == 'processing':
- sockets.postprocessing(d['postprocessor'])
- elif d['status'] == 'finished':
- sockets.finished_download()
- except Empty:
- pass
- finally:
- # Sleep between checking for updates
- sleep(0.1)
-
- @staticmethod
- def loop_in_thread(loop, future):
- loop.run_until_complete(future)
-
- @staticmethod
- def fetch_video(video, templates, metadata_sources, defaulttemplate):
- sb = findsegments(video["webpage_url"])
- segments = sb if type(sb) == list else 'error'
- env = Environment(
- loader=PackageLoader('metatube'),
- autoescape=select_autoescape()
- )
- downloadtemplate = env.get_template('downloadform.html')
- metadatatemplate = env.get_template('metadataform.html')
- downloadform = downloadtemplate.render(templates=templates, segments=segments, default=defaulttemplate)
- metadataform = metadatatemplate.render(metadata_sources=metadata_sources)
- sockets.youtuberesults(video, downloadform, metadataform)
\ No newline at end of file
diff --git a/metatube/youtube/download.py b/metatube/youtube/download.py
new file mode 100644
index 00000000..904bac2f
--- /dev/null
+++ b/metatube/youtube/download.py
@@ -0,0 +1,96 @@
+import yt_dlp, os
+from yt_dlp.postprocessor.ffmpeg import FFmpegPostProcessorError
+from threading import Thread
+from urllib.error import URLError
+from yt_dlp.utils import ExtractorError, DownloadError, PostProcessingError
+from metatube import sockets, logger
+from functools import partial
+from queue import LifoQueue, Empty
+from time import sleep
+import asyncio
+
+class download(object):
+ @staticmethod
+ async def download(url: list, queue: LifoQueue, ytdl_options: dict):
+ download_hook_partial = partial(download.download_hook, queue)
+ postprocessor_hook_partial = partial(download.postprocessor_hook, queue)
+ ytdl_options['progress_hooks'] = [download_hook_partial]
+ ytdl_options['postprocessor_hooks'] = [postprocessor_hook_partial]
+ with yt_dlp.YoutubeDL(ytdl_options) as ytdl:
+ try:
+ return ytdl.download(url)
+ except KeyError as e:
+ logger.error('%s key did not exist', str(e))
+ sockets.downloaderrors({'status': 'error', 'message': 'The output template was incorrect. Check logs for more info.'})
+ return None
+ except ExtractorError as e:
+ logger.error('Extractor error: %s', str(e))
+ sockets.downloaderrors({'status': 'error', 'message': 'An extractor error has occured. Check logs for more info.'})
+ return None
+ except FFmpegPostProcessorError as e:
+ logger.error('FFmpegPostProcessor error: %s', str(e))
+ sockets.downloaderrors({'status': 'error', 'message': 'An processing error involving FFmpeg has occured. Check logs for more info.'})
+ return None
+ except PostProcessingError as e:
+ logger.error('Postprocessor error: %s', str(e))
+ sockets.downloaderrors({'status': 'error', 'message': 'A processing error has occured. Check logs for more info.'})
+ return None
+ except DownloadError as e:
+ logger.error('Downloading error: %s', str(e))
+ sockets.downloaderrors({'status': 'error', 'message': 'A downloading error has occured. Check logs for more info.'})
+ return None
+ except URLError as e:
+ logger.error('Network connection error: %s', str(e))
+ sockets.downloaderrors({'status': 'error', 'message': 'A network error occured. Check logs for more info.'})
+ return None
+ except Exception as e:
+ logger.exception('Error during downloading video: %s', str(e))
+ sockets.downloaderrors({'status': 'error', 'message': 'Something has gone wrong. Check logs for more info'})
+ return None
+
+ @staticmethod
+ def download_hook(queue: LifoQueue, d: dict):
+ queue.put(d)
+
+ @staticmethod
+ def postprocessor_hook(queue: LifoQueue, d: dict):
+ if d['status'] == 'processing':
+ sockets.postprocessing(d['postprocessor'])
+ elif d['status'] == 'finished':
+ if d['postprocessor'] == 'MoveFiles':
+ sockets.finished_postprocessor(d['postprocessor'], d['info_dict']['filepath'])
+ queue.put({'status': 'mergedata', 'filepath': d['info_dict']['filepath']})
+
+ @staticmethod
+ def start_download(url, ytdl_options, queue: LifoQueue):
+ loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(loop)
+ coros = [download.download(url, queue, ytdl_options)]
+ future = asyncio.gather(*coros)
+ thread = Thread(target=download.loop_in_thread, args=[loop, future])
+ thread.start()
+ # While the future isn't finished yet continue
+ while not future.done():
+ try:
+ # Get the latest status update from the queue and print it
+ d = queue.get_nowait()
+ if d['status'] == 'downloading':
+ if "total_bytes_estimate" in d:
+ downloaded_bytes = d['downloaded_bytes'] or 'Unknown'
+ total_bytes = d['total_bytes_estimate'] or d['total_bytes'] or 'Unknown'
+ sockets.downloadprogress(downloaded_bytes, total_bytes)
+ elif d['status'] == 'processing':
+ sockets.postprocessing(d['postprocessor'])
+ elif d['status'] == 'finished':
+ sockets.finished_download()
+ elif d['status'] == 'mergedata':
+ break
+ except Empty:
+ pass
+ finally:
+ # Sleep between checking for updates
+ sleep(0.1)
+
+ @staticmethod
+ def loop_in_thread(loop, future):
+ loop.run_until_complete(future)
\ No newline at end of file
diff --git a/metatube/youtube/downloadExceptions.py b/metatube/youtube/downloadExceptions.py
new file mode 100644
index 00000000..f89ecd9e
--- /dev/null
+++ b/metatube/youtube/downloadExceptions.py
@@ -0,0 +1,12 @@
+from metatube.Exception import MetaTubeException
+class EmptyFragments(MetaTubeException):
+ pass
+
+class NoOutputType(MetaTubeException):
+ pass
+
+class InvalidHardwareTranscoding(MetaTubeException):
+ pass
+
+class InvalidYouTubeUrl(MetaTubeException):
+ pass
\ No newline at end of file
diff --git a/metatube/youtube/downloadOptions.py b/metatube/youtube/downloadOptions.py
new file mode 100644
index 00000000..a7549437
--- /dev/null
+++ b/metatube/youtube/downloadOptions.py
@@ -0,0 +1,146 @@
+import os
+from metatube import logger
+from .downloadExceptions import *
+from metatube.metadata.metadataObject import MetadataObject
+from yt_dlp.postprocessor.metadataparser import MetadataParserPP
+class downloadOptions(object):
+ def __init__(
+ self,
+ ext: str,
+ output_folder: str,
+ output_type: str,
+ output_format: str,
+ bitrate: int | str,
+ skipfragments: dict,
+ proxy_data: dict,
+ ffmpeg: str,
+ hw_transcoding: str,
+ vaapi_device: str,
+ width: int | str,
+ height: int | str,
+ verbose: bool,
+
+ ):
+ self.ext = ext
+ self.output_folder = output_folder
+ self.output_type = output_type
+ self.output_format = output_format
+ self.bitrate = bitrate
+ self.skipfragments = skipfragments
+ self.proxy_data = proxy_data
+ self.ffmpeg = ffmpeg
+ self.hw_transcoding = hw_transcoding
+ self.vaapi_device = vaapi_device
+ self.width = width
+ self.height = height
+ self.verbose = verbose
+
+ def optionsToDictMapper(self, metadata: MetadataObject) -> dict:
+ filepath = os.path.join(self.output_folder, self.output_format)
+ postprocessors = []
+ postprocessor_args = {}
+ proxy_string = ""
+ ext = "m4a" if "m4a" in self.ext else self.ext
+ '''
+ Audio:
+ If an audio type has been selected, first try to look for a format with the selected extension
+ If no audio format with the selected extension has been found, just look for the best audio format
+ and automatically convert it to the selected extension anyway
+ Video:
+ Exactly the same for videos
+ '''
+ format = f'ba[ext={ext}]/ba' if self.output_type == 'Audio' else f'b[ext={ext}]/ba+bv[ext={ext}]/b/ba+bv'
+
+ # choose whether to use the FFmpegExtractAudio post processor or the FFmpegVideoConverter one
+ if self.output_type == 'Audio':
+ postprocessors.append({
+ "key": "FFmpegExtractAudio",
+ "preferredcodec": ext,
+ "preferredquality": self.bitrate
+ })
+ elif self.output_type == 'Video':
+ postprocessors.append({
+ "key": "FFmpegVideoConvertor",
+ "preferedformat": ext
+ })
+ postprocessor_args['videoconvertor'] = []
+ if self.bitrate != 'best':
+ postprocessor_args["videoconvertor"] = ['-b:a', str(self.bitrate) + "k"]
+
+ if self.height != 'best' and self.width != 'best':
+ postprocessor_args["videoconvertor"][:0] = ['-vf', 'scale=' + str(self.width) + ':' + str(self.height)]
+
+ # If hardware transcoding isn't None, add a hardware transcoding thingy to the FFmpeg arguments
+ if self.hw_transcoding != 'None':
+ if "videoconvertor" not in postprocessor_args:
+ postprocessor_args["videoconvertor"] = []
+ if self.hw_transcoding == 'nvenc':
+ postprocessor_args["videoconvertor"].extend(['-c:v', 'h264_nvenc'])
+ elif self.hw_transcoding == 'qsv':
+ postprocessor_args["videoconvertor"].extend(['-c:v', 'h264_qsv'])
+ elif self.hw_transcoding == 'videotoolbox':
+ postprocessor_args["videoconvertor"].extend(['-c:v', 'h264_videotoolbox'])
+ elif 'vaapi' in self.hw_transcoding:
+ postprocessor_args["videoconvertor"].extend(['-vaapi_device', self.vaapi_device, '-c:v', 'h264_vaapi'])
+ elif self.hw_transcoding == 'amd':
+ postprocessor_args["videoconvertor"].extend(['-c:v', 'h264_amf'])
+ elif self.hw_transcoding == 'omx':
+ postprocessor_args["videoconvertor"].extend(['-c:v', 'h264_omx'])
+ else:
+ raise InvalidHardwareTranscoding("An invalid type has been selected for hardware transcoding.")
+ else:
+ raise NoOutputType("No output type has been selected. Video or audio must be chosen for download.")
+
+ # If segments have been submitted by the user to exclude, add a ModifyChapters key and add ranges
+ if len(self.skipfragments) > 0:
+ ranges = []
+ for segment in self.skipfragments:
+ if len(segment["start"]) < 1 or len(segment["end"]) < 1:
+ raise EmptyFragments("One or more segment fields was left empty.")
+ else:
+ ranges.append((int(segment["start"]), int(segment["end"])))
+ postprocessors.append({
+ 'key': 'ModifyChapters',
+ 'remove_ranges': ranges
+ })
+
+ '''
+ --parse-metadata example in CLI:
+ yt-dlp orJSJGHjBLI -x --audio-format mp3 --add-metadata -o "%(track,title)s - %(artist)s.%(ext)s" --parse-metadata " Bad Habits: %(title)s" --parse-metadata "Ed Sheeran:%(artist)s"
+ '''
+ postprocessors.append({
+ 'actions': [
+ (MetadataParserPP.interpretter, " " + metadata.title, ' %(title)s'),
+ (MetadataParserPP.interpretter, metadata.album, '%(album)s'),
+ (MetadataParserPP.interpretter, metadata.album_artists, '%(artist)s'),
+ (MetadataParserPP.interpretter, metadata.album_artists, '%(album_artist)s'),
+ (MetadataParserPP.interpretter, str(metadata.tracknr), '%(track_number)s'),
+ (MetadataParserPP.interpretter, metadata.language, '%(language)s'),
+ (MetadataParserPP.interpretter, metadata.genres, '%(genre)s'),
+ (MetadataParserPP.interpretter, metadata.release_date, '%(date)s'),
+ ],
+ 'key': 'MetadataParser',
+ 'when': 'pre_process'
+ })
+
+ ytdl_options = {
+ 'format': format,
+ 'merge_output_format': ext,
+ 'postprocessors': postprocessors,
+ 'postprocessor_args': postprocessor_args,
+ 'ffmpeg_location': self.ffmpeg,
+ 'logger': logger,
+ 'outtmpl': filepath,
+ 'noplaylist': True,
+ 'verbose': self.verbose
+ }
+
+ # Add proxy if proxy is enabled
+ if self.proxy_data['proxy_type'] != 'None':
+ proxy_string = self.proxy_data["proxy_type"].lower().strip() + "://"
+ if len(self.proxy_data["proxy_username"]) > 0 and len(self.proxy_data["proxy_username"]) > 0:
+ proxy_string += self.proxy_data["proxy_username"] + ":" + self.proxy_data["proxy_password"] + "@" + self.proxy_data["proxy_address"].strip() + ":" + self.proxy_data["proxy_port"].strip()
+ else:
+ proxy_string += self.proxy_data["proxy_address"].strip() + ":" + self.proxy_data["proxy_port"].strip()
+ ytdl_options["proxy"] = proxy_string
+ return ytdl_options
\ No newline at end of file
diff --git a/metatube/youtube/manageDownloadProcess.py b/metatube/youtube/manageDownloadProcess.py
new file mode 100644
index 00000000..ba8662bf
--- /dev/null
+++ b/metatube/youtube/manageDownloadProcess.py
@@ -0,0 +1,47 @@
+from .downloadOptions import downloadOptions
+from .download import download
+from metatube.metadata.processMetadata import processMetadata
+from metatube.metadata.mergeMetadata import mergeMetadata
+from metatube.sockets import metadata_error, downloaderrors
+from metatube.Exception import MetaTubeException
+from threading import Thread
+from queue import Empty, LifoQueue
+from time import sleep
+class manageDownloadProcess(object):
+ def __init__(self, downloadOptions: downloadOptions, metadataProcessor: processMetadata, url: str, goal: 'str'):
+ self.downloadOptions = downloadOptions
+ self.metadataProcessor = metadataProcessor
+ self.url = url
+ self.goal = goal
+
+ def start_download(self):
+ try:
+ metadata = self.metadataProcessor.getMetadata()
+ except MetaTubeException as error:
+ metadata_error(str(error))
+ return
+ try:
+ yt_dlpOptions = self.downloadOptions.optionsToDictMapper(metadata)
+ except MetaTubeException as error:
+ downloaderrors(str(error))
+ return
+ queue = LifoQueue()
+ downloadProcess = Thread(target=download.start_download, args=[self.url, yt_dlpOptions, queue])
+ downloadProcess.start()
+ downloadProcess.join()
+ try:
+ lastItem = queue.get_nowait()
+ filepath = lastItem['filepath']
+ merge = mergeMetadata(filepath, self.goal, metadata=metadata)
+ if self.downloadOptions.ext in ['MP3', 'OPUS', 'FLAC', 'OGG']:
+ merge.mergeaudiodata()
+ elif self.downloadOptions.ext in ['MP4', 'M4A']:
+ merge.mergevideodata()
+ elif self.downloadOptions.ext in ['WAV']:
+ merge.mergeid3data()
+ except Empty:
+ pass
+ except MetaTubeException as error:
+ metadata_error(str(error))
+ finally:
+ sleep(0.1)
\ No newline at end of file
diff --git a/metatube/youtube/youtubeUtils.py b/metatube/youtube/youtubeUtils.py
new file mode 100644
index 00000000..c3aee4a4
--- /dev/null
+++ b/metatube/youtube/youtubeUtils.py
@@ -0,0 +1,59 @@
+import yt_dlp
+from youtubesearchpython import VideosSearch
+from .downloadExceptions import InvalidYouTubeUrl
+from metatube import sockets, logger
+from metatube.sponsorblock import segments as findsegments
+from jinja2 import Environment, PackageLoader, select_autoescape
+
+class utils(object):
+ @staticmethod
+ def is_supported(url):
+ extractors = yt_dlp.extractor.gen_extractors()
+ for e in extractors:
+ if e.suitable(url) and e.IE_NAME == 'youtube':
+ return True
+ return False
+
+ @staticmethod
+ def fetch_url(url, verbose):
+ if utils.is_supported(url):
+ ytdl_options = {'logger': logger, 'verbose': verbose}
+ with yt_dlp.YoutubeDL(ytdl_options) as ytdl:
+ try:
+ info = ytdl.extract_info(url, download=False)
+ return info
+ except Exception as e:
+ return str(e)
+ else:
+ raise InvalidYouTubeUrl("Invalid URL!")
+
+ @staticmethod
+ def verifytemplate(template, info_dict, verbose):
+ ytdl_options = {'logger': logger, 'verbose': verbose}
+ with yt_dlp.YoutubeDL(ytdl_options) as ytdl:
+ try:
+ filename = ytdl.evaluate_outtmpl(template, info_dict)
+ return filename
+ except Exception as e:
+ return str(e)
+
+ @staticmethod
+ def search(query: str):
+ logger.info('Searching YouTube for \'%s\'', query)
+ search = VideosSearch(query)
+ result = search.result()
+ sockets.youtubesearch(result)
+
+ @staticmethod
+ def fetch_video(video, templates, metadata_sources, defaulttemplate):
+ sb = findsegments(video["webpage_url"])
+ segments = sb if type(sb) == list else 'error'
+ env = Environment(
+ loader=PackageLoader('metatube'),
+ autoescape=select_autoescape()
+ )
+ downloadtemplate = env.get_template('downloadform.html')
+ metadatatemplate = env.get_template('metadataform.html')
+ downloadform = downloadtemplate.render(templates=templates, segments=segments, default=defaulttemplate)
+ metadataform = metadatatemplate.render(metadata_sources=metadata_sources)
+ sockets.youtuberesults(video, downloadform, metadataform)
\ No newline at end of file
diff --git a/tests/testDatabase.py b/tests/testDatabase.py
index cfb86fe6..f89f2129 100644
--- a/tests/testDatabase.py
+++ b/tests/testDatabase.py
@@ -184,8 +184,8 @@ def testDatabase(self):
'album': 'Whenever You Need Somebody',
'date': '12-11-1987',
'image': 'https://i.scdn.co/image/ab67616d0000b273baf89eb11ec7c657805d2da0',
- 'track_id': '4cOdK2wGLETKBW3PvgPWqT',
- 'ytid': 'dQw4w9WgXcQ'
+ 'songid': '4cOdK2wGLETKBW3PvgPWqT',
+ 'youtube_id': 'dQw4w9WgXcQ'
})
item = Database.fetchitem(itemId)
@@ -201,12 +201,12 @@ def testDatabase(self):
'date': '1987-12-11 00:00:00',
'length': 'None',
'cover': 'https://i.scdn.co/image/ab67616d0000b273baf89eb11ec7c657805d2da0',
- 'audio_id': '4cOdK2wGLETKBW3PvgPWqT',
+ 'songid': '4cOdK2wGLETKBW3PvgPWqT',
'youtube_id': 'dQw4w9WgXcQ'
})
self.assertIs(Database.checkfile(item.filepath), item) # type: ignore
self.assertIs(Database.checkyt(item.youtube_id), item) # type: ignore
- self.assertIs(Database.checktrackid(item.audio_id), item) # type: ignore
+ self.assertIs(Database.songidexists(item.songid), item) # type: ignore
item.update({ # type: ignore
'filepath': os.path.join(self.app.config['DOWNLOADS'], '/test.mp3'),
@@ -215,7 +215,7 @@ def testDatabase(self):
'album': 'Some album',
'date': datetime.now().date(),
'image': '/path/to/cover.png',
- 'track_id': 'someid',
+ 'songid': 'someid',
'youtube_id': 'y6120QOlsfU',
'length': None
})
@@ -229,7 +229,7 @@ def testDatabase(self):
'date': datetime.now().strftime("%Y-%m-%d 00:00:00"),
'length': 'None',
'cover': '/path/to/cover.png',
- 'audio_id': 'someid',
+ 'songid': 'someid',
'youtube_id': 'y6120QOlsfU'
})
From 7cc60520f86f499f3a72039121b3e50f9c6a8715 Mon Sep 17 00:00:00 2001
From: JVT038 <47184046+JVT038@users.noreply.github.com>
Date: Fri, 2 Feb 2024 13:40:09 +0100
Subject: [PATCH 02/11] Refactoring, fixed some bugs and improved the process
---
metatube/DatabaseExceptions.py | 4 ++
metatube/database.py | 8 +++-
metatube/genius.py | 3 +-
metatube/metadata/MetadataExceptions.py | 12 +++++
metatube/metadata/mergeMetadata.py | 8 ++--
metatube/metadata/metadataObject.py | 2 -
metatube/metadata/processMetadata.py | 52 +++++++++++++-------
metatube/overview/routes.py | 58 +++++++++++++----------
metatube/sockets.py | 16 +++----
metatube/spotify.py | 2 +
metatube/static/JS/overview.js | 45 ++++++++++--------
metatube/youtube/download.py | 14 +++---
metatube/youtube/downloadOptions.py | 10 ++--
metatube/youtube/manageDownloadProcess.py | 37 ++++++++++-----
14 files changed, 167 insertions(+), 104 deletions(-)
create mode 100644 metatube/DatabaseExceptions.py
diff --git a/metatube/DatabaseExceptions.py b/metatube/DatabaseExceptions.py
new file mode 100644
index 00000000..cb62e37c
--- /dev/null
+++ b/metatube/DatabaseExceptions.py
@@ -0,0 +1,4 @@
+from .Exception import MetaTubeException
+
+class InvalidItemId(MetaTubeException):
+ pass
\ No newline at end of file
diff --git a/metatube/database.py b/metatube/database.py
index fa070ee6..94c9b9f8 100644
--- a/metatube/database.py
+++ b/metatube/database.py
@@ -1,4 +1,5 @@
from metatube import db, logger, sockets
+from .DatabaseExceptions import *
from sqlalchemy.sql import expression
from dateutil import parser
@@ -187,7 +188,10 @@ def getrecords():
@staticmethod
def fetchitem(input_id):
- return Database.query.filter_by(id = input_id).first()
+ item = Database.query.filter_by(id = input_id).first()
+ if item is None:
+ raise InvalidItemId("Invalid item ID")
+ return item
@staticmethod
def checkfile(filepath_input):
@@ -208,7 +212,7 @@ def insert(data):
row = Database(
filepath = data["filepath"],
name = data["name"],
- artist = '; '.join(data["artist"]),
+ artist = data["artist"],
album = data["album"],
date = parser.parse(data["date"]),
cover = data["image"],
diff --git a/metatube/genius.py b/metatube/genius.py
index 361a8960..375791f7 100644
--- a/metatube/genius.py
+++ b/metatube/genius.py
@@ -4,8 +4,9 @@ class Genius():
def __init__(self, client_id):
try:
self.genius = geniusobj(client_id)
- except TypeError() as e:
+ except TypeError as e:
logger.error('Genius API failed: %s', str(e))
+ raise e from e
def search(self, data):
search = self.genius.search_songs(data["title"], data["max"])
diff --git a/metatube/metadata/MetadataExceptions.py b/metatube/metadata/MetadataExceptions.py
index fb1189a5..b2066bed 100644
--- a/metatube/metadata/MetadataExceptions.py
+++ b/metatube/metadata/MetadataExceptions.py
@@ -25,4 +25,16 @@ class NoMetadataAPIResult(MetaTubeException):
pass
class NoMetadataFound(MetaTubeException):
+ pass
+
+class InvalidSpotifyCredentials(MetaTubeException):
+ pass
+
+class NoSpotifyCredentails(MetaTubeException):
+ pass
+
+class NoGeniusToken(MetaTubeException):
+ pass
+
+class InvalidGeniusToken(MetaTubeException):
pass
\ No newline at end of file
diff --git a/metatube/metadata/mergeMetadata.py b/metatube/metadata/mergeMetadata.py
index bb7b09ca..6ed38f79 100644
--- a/metatube/metadata/mergeMetadata.py
+++ b/metatube/metadata/mergeMetadata.py
@@ -151,7 +151,7 @@ def mergeaudiodata(self):
sockets.overview({'msg': 'changed_metadata', 'data': response})
elif self.goal == 'add':
logger.info('Finished adding metadata to %s', self.metadata.title)
- sockets.finished_metadata(response)
+ return response
def mergeid3data(self):
if self.metadata.extension == 'WAV':
@@ -183,7 +183,7 @@ def mergeid3data(self):
sockets.overview({'msg': 'changed_metadata', 'data': response})
elif self.goal == 'add':
logger.info('Finished adding metadata to %s', self.metadata.title)
- sockets.finished_metadata(response)
+ return response
def mergevideodata(self):
if self.metadata.extension in ['M4A', 'MP4']:
@@ -214,7 +214,7 @@ def mergevideodata(self):
sockets.overview({'msg': 'changed_metadata', 'data': response})
elif self.goal == 'add':
logger.info('Finished adding metadata to %s', self.metadata.title)
- sockets.finished_metadata(response)
+ return response
@staticmethod
def readaudiometadata(filename):
@@ -295,7 +295,7 @@ def metadataResponseMapper(self, length) -> dict:
'album': self.metadata.album,
'date': self.metadata.release_date,
'length': length,
- 'image': self.metadata.cover,
+ 'image': self.metadata.cover_path,
'songid': self.metadata.songid
}
diff --git a/metatube/metadata/metadataObject.py b/metatube/metadata/metadataObject.py
index 9a51fa7b..edc3c3ea 100644
--- a/metatube/metadata/metadataObject.py
+++ b/metatube/metadata/metadataObject.py
@@ -10,7 +10,6 @@ def __init__(
release_date: str | None,
songid: str | None,
albumid: str | None,
- album_artists: str | None,
tracknr: int | None,
total_tracks: int | None,
cover: bytes,
@@ -30,7 +29,6 @@ def __init__(
self.release_date = release_date or ''
self.albumid = albumid or ''
self.songid = songid or ''
- self.album_artists = album_artists or ''
self.tracknr = tracknr or 1
self.total_tracks = total_tracks or 1
self.cover = cover
diff --git a/metatube/metadata/processMetadata.py b/metatube/metadata/processMetadata.py
index 464614e5..c4769284 100644
--- a/metatube/metadata/processMetadata.py
+++ b/metatube/metadata/processMetadata.py
@@ -1,18 +1,27 @@
import json
from magic import Magic
+from metatube.spotify import spotify_metadata as Spotify
+from metatube.genius import Genius
from re import M
from metatube import Config as env, logger
from metatube import musicbrainz
from metatube.deezer import Deezer
+from metatube.database import Config
from .metadataObject import MetadataObject
-from .MetadataExceptions import InvalidCoverURL, NoMetadataAPIResult, NoMetadataFound
+from .MetadataExceptions import (
+ InvalidCoverURL,
+ NoMetadataAPIResult,
+ NoMetadataFound,
+ NoSpotifyCredentails,
+ InvalidSpotifyCredentials,
+ NoGeniusToken,
+ InvalidGeniusToken
+)
import requests, os
class processMetadata(object):
- def __init__(self, usermetadata, extension, genius = None, spotify = None):
+ def __init__(self, usermetadata, extension):
self.usermetadata = usermetadata
- self.genius = genius
- self.spotify = spotify
# self.title = usermetadata['title'] or None
# self.artists = usermetadata['artists'] or None
# self.album = usermetadata['album'] or None
@@ -28,25 +37,40 @@ def __init__(self, usermetadata, extension, genius = None, spotify = None):
self.extension = extension
- def getMetadata(self) -> MetadataObject:
+ def getMetadata(self, app) -> MetadataObject:
metadata = None
if self.source == 'Spotify':
- metadata_source = self.spotify.fetch_track(self.songid) # type: ignore
+ with app.app_context():
+ cred = Config.get_spotify()
+ if str(cred) == '':
+ raise NoSpotifyCredentails("Spotify was selected as source, however there are no API credentials for the Spotify API.")
+ try:
+ spotify = Spotify(cred.split(';')[1], cred.split(';')[0])
+ except InvalidSpotifyCredentials as exception:
+ raise exception from exception
+ metadata_source = spotify.fetch_track(self.songid) # type: ignore
if metadata_source is None:
- raise NoMetadataAPIResult("There was no result from the selected metadata API.")
+ raise NoMetadataAPIResult("There was no result from the Spotify API.")
metadata = self.getspotifydata(metadata_source)
elif self.source == 'Musicbrainz':
metadata_source = musicbrainz.search_id_release(self.songid)
if metadata_source is None:
- raise NoMetadataAPIResult("There was no result from the selected metadata API.")
+ raise NoMetadataAPIResult("There was no result from the Musicbrainz API.")
metadata = self.getmusicbrainzdata(metadata_source)
elif self.source == 'Deezer':
metadata_source = Deezer.searchid(self.songid) # type: ignore
if metadata_source is None:
- raise NoMetadataAPIResult("There was no result from the selected metadata API.")
+ raise NoMetadataAPIResult("There was no result from the Deezer API.")
metadata = self.getdeezerdata(metadata_source)
elif self.source == 'Genius':
- metadata_source = self.genius.fetchsong(self.songid) # type: ignore
+ token = Config.get_genius()
+ if str(token) == '':
+ raise NoGeniusToken("Genius was selected as source, however there is no API token for the Genius API.")
+ try:
+ genius = Genius(token)
+ metadata_source = genius.fetchsong(self.songid) # type: ignore
+ except Exception:
+ raise InvalidGeniusToken("Invalid Genius API token.")
if metadata_source is None:
raise NoMetadataAPIResult("There was no result from the selected metadata API.")
lyrics = genius.fetchlyrics(metadata_source["song"]["url"]) # type: ignore
@@ -88,7 +112,6 @@ def getmusicbrainzdata(self, metadata_source) -> MetadataObject | None:
magic = Magic(mime=True)
cover_mime_type = magic.from_buffer(image)
except Exception:
- # sockets.metadata_error('Cover URL is invalid!')
raise InvalidCoverURL("Cover URL is invalid!")
else:
cover_mime_type = "image/png"
@@ -131,7 +154,6 @@ def getmusicbrainzdata(self, metadata_source) -> MetadataObject | None:
release_date,
mbp_songid,
mbp_albumid,
- '',
int(tracknr),
int(total_tracks),
image,
@@ -169,8 +191,6 @@ def getspotifydata(self, metadata_source) -> MetadataObject | None:
magic = Magic(mime=True)
cover_mime_type = magic.from_buffer(image)
except Exception:
- # sockets.metadata_error('Cover URL is invalid!')
- # logger.warning('Cover URL submitted by the user was invalid')
raise InvalidCoverURL("Cover URL is invalid!")
else:
cover_mime_type = "image/png"
@@ -186,7 +206,6 @@ def getspotifydata(self, metadata_source) -> MetadataObject | None:
release_date,
songid,
albumid,
- '',
int(tracknr),
int(total_tracks),
image,
@@ -238,7 +257,6 @@ def getdeezerdata(self, metadata_source) -> MetadataObject | None:
release_date,
songid,
albumid,
- '',
int(tracknr),
int(total_tracks),
image,
@@ -290,7 +308,6 @@ def getgeniusdata(self, metadata_source, lyrics) -> MetadataObject | None:
release_date,
songid,
albumid,
- '',
int(tracknr),
int(total_tracks),
image,
@@ -328,7 +345,6 @@ def onlyuserdata(self) -> MetadataObject | None:
self.usermetadata.get('album_releasedate', ''),
self.usermetadata.get('songid', ''),
self.usermetadata.get('albumid', ''),
- '',
self.usermetadata.get('album_tracknr', '1'),
self.usermetadata.get('album_tracknr', '1'),
image,
diff --git a/metatube/overview/routes.py b/metatube/overview/routes.py
index 65229049..ab098e5d 100644
--- a/metatube/overview/routes.py
+++ b/metatube/overview/routes.py
@@ -2,6 +2,7 @@
from magic import Magic
from metatube.overview import bp
from metatube.database import *
+from metatube.DatabaseExceptions import *
from metatube.youtube.manageDownloadProcess import manageDownloadProcess
from metatube.youtube.youtubeUtils import utils as ytutils
from metatube.youtube.downloadOptions import downloadOptions
@@ -113,20 +114,11 @@ def searchmetadata(data):
@socketio.on('ytdl_download')
def download(data):
if Database.songidexists(data['userMetadata']['songid']) is True:
- return 'duplicate'
- url = data["url"]
+ sockets.downloadprocesserror("This song has already been downloaded.")
+ return
ext = str(data["ext"]).upper() or 'MP3'
- genius = None
- spotify = None
- if data['userMetadata']['metadata_source'] == 'Spotify':
- credentials = Config.get_spotify().split(';')
- spotify = Spotify(credentials[1], credentials[0])
- elif data['userMetadata']['metadata_source'] == 'Genius':
- token = Config.get_genius()
- genius = Genius(token)
-
- processedMetadata = processMetadata(data['userMetadata'], ext, genius, spotify)
+ processedMetadata = processMetadata(data['userMetadata'], ext)
output_folder = data["output_folder"] or '/downloads'
output_type = data["type"] or 'Audio'
output_format = data["output_format"] or f'%(title)s.%(ext)s'
@@ -135,16 +127,33 @@ def download(data):
proxy_data = json.loads(data["proxy_data"]) or {'proxy_type': 'None'}
width = data["width"] or 'best'
height = data["height"] or 'best'
+ youtube_id = data['youtube_id']
ffmpeg = Config.get_ffmpeg()
hw_transcoding = Config.get_hwt()
vaapi_device = hw_transcoding.split(';')[1] if 'vaapi' in hw_transcoding else ''
verbose = True if str(env.LOGGER).lower() == 'true' else False
- ytdl_options = downloadOptions(ext, output_folder, output_type, output_format, bitrate, skipfragments, proxy_data, ffmpeg, hw_transcoding, vaapi_device, width, height, verbose)
+ ytdl_options = downloadOptions(
+ youtube_id,
+ ext,
+ output_folder,
+ output_type,
+ output_format,
+ bitrate,
+ skipfragments,
+ proxy_data,
+ ffmpeg,
+ hw_transcoding,
+ vaapi_device,
+ width,
+ height,
+ verbose
+ )
if isinstance(ytdl_options, downloadOptions):
- logger.info('Request to download %s', data["url"])
- downloadProcess = manageDownloadProcess(ytdl_options, processedMetadata, url, 'add')
- socketio.start_background_task(downloadProcess.start_download)
+ logger.info('Request to download %s', data["youtube_id"])
+ downloadProcess = manageDownloadProcess(ytdl_options, processedMetadata, 'add')
+
+ socketio.start_background_task(downloadProcess.start_download, current_app._get_current_object()) # type: ignore
return 'OK'
@socketio.on('fetchmbprelease')
@@ -192,12 +201,6 @@ def fetchgeniusalbum(input_id):
token = Config.get_genius()
genius = Genius(token)
genius.fetchalbum(input_id)
-
-@socketio.on('insertitem')
-def insertitem(data):
- id = Database.insert(data)
- data["id"] = id
- sockets.overview({'msg': 'inserted_song', 'data': data})
@socketio.on('updateitem')
def updateitem(data):
@@ -209,9 +212,12 @@ def updateitem(data):
data["date"] = parser.parse(data["date"])
except Exception:
data["date"] = datetime.now().date()
- item = Database.fetchitem(id)
- data["youtube_id"] = item.youtube_id
- item.update(data)
+ try:
+ item = Database.fetchitem(id)
+ data["youtube_id"] = item.youtube_id
+ item.update(data)
+ except InvalidItemId:
+ return
@socketio.on('downloaditems')
def downloaditems(items):
@@ -446,7 +452,7 @@ def editfilerequest(filepath, id):
magic = Magic(mime=True)
mime_type = magic.from_buffer(image)
except Exception:
- sockets.downloaderrors('Cover URL is invalid!')
+ sockets.downloadprocesserror('Cover URL is invalid!')
return False
else:
file = open(item.cover, 'rb')
diff --git a/metatube/sockets.py b/metatube/sockets.py
index 49787d20..d3b83f0e 100644
--- a/metatube/sockets.py
+++ b/metatube/sockets.py
@@ -60,6 +60,9 @@ def deezersearch(data):
def deezertrack(data):
socketio.emit('deezer_track', data)
+def downloadprocesserror(message: str):
+ socketio.emit('downloadprocesserror', message)
+
def downloadprogress(downloaded_bytes, total_bytes):
socketio.emit('downloadprogress', {
'status': 'downloading',
@@ -68,7 +71,7 @@ def downloadprogress(downloaded_bytes, total_bytes):
})
def postprocessing(postprocessor):
- socketio.emit('postprocessing', {'postprocessor': postprocessor})
+ socketio.emit('postprocessing', postprocessor)
def finished_postprocessor(postprocessor, filepath):
socketio.emit('finished_postprocessor', {
@@ -79,11 +82,8 @@ def finished_postprocessor(postprocessor, filepath):
def finished_download():
socketio.emit('finished_download')
-def finished_metadata(response):
- socketio.emit('finished_metadata', {'status':'finished_metadata', 'data': response})
-
-def metadata_error(message):
- socketio.emit('downloaderror', message)
+def finished_metadata(data):
+ socketio.emit('finished_metadata', data)
-def downloaderrors(message):
- socketio.emit('downloaderror', message)
\ No newline at end of file
+def inserted_song(data):
+ socketio.emit('inserted_song', data)
\ No newline at end of file
diff --git a/metatube/spotify.py b/metatube/spotify.py
index cf998c03..cbc68d47 100644
--- a/metatube/spotify.py
+++ b/metatube/spotify.py
@@ -1,6 +1,7 @@
import spotipy
from spotipy.oauth2 import SpotifyClientCredentials, SpotifyOauthError
from metatube import sockets, logger
+from metatube.metadata.MetadataExceptions import InvalidSpotifyCredentials
class spotify_metadata():
def __init__(self, id, secret):
@@ -8,6 +9,7 @@ def __init__(self, id, secret):
self.spotify = spotipy.Spotify(auth_manager=SpotifyClientCredentials(client_id=id, client_secret=secret))
except SpotifyOauthError as e:
logger.error('Spotify authentication has failed. Error: %s', str(e))
+ raise InvalidSpotifyCredentials('Spotify authentication has failed. Error: %s', str(e))
def search(self, data):
searchresults = self.spotify.search(f"track:{data['title']}", data["max"])
diff --git a/metatube/static/JS/overview.js b/metatube/static/JS/overview.js
index 82137fc3..070fd3cd 100644
--- a/metatube/static/JS/overview.js
+++ b/metatube/static/JS/overview.js
@@ -390,8 +390,7 @@ $(document).ready(function() {
}
return n
}
- let itemdata = data.data;
- let dateobj = new Date(itemdata["date"]);
+ let dateobj = new Date(data["date"]);
let date = addLeadingZeros(dateobj.getDate()) + "-" + addLeadingZeros(dateobj.getMonth() + 1) + "-" + dateobj.getFullYear();
let tr = document.createElement('tr');
let td_name = document.createElement('td');
@@ -412,18 +411,18 @@ $(document).ready(function() {
let namespan = document.createElement('span');
let namerow = document.createElement('div');
- td_artist.innerText = itemdata["artist"]
- td_album.innerText = itemdata["album"];
+ td_artist.innerText = data["artist"]
+ td_album.innerText = data["album"];
td_date.innerText = date;
- td_ext.innerText = itemdata["filepath"].split('.')[itemdata["filepath"].split('.').length - 1].toUpperCase();
+ td_ext.innerText = data["filepath"].split('.')[data["filepath"].split('.').length - 1].toUpperCase();
dropdownbtn.innerText = 'Select action';
- namespan.innerText = itemdata["name"];
+ namespan.innerText = data["name"];
- namerow.classList.add('row', 'd-flex', 'justify-content-center');
+ namerow.classList.add('row');
namecol.classList.add('align-self-center');
namespan.classList.add('align-middle');
if($(window).width() > 991) {
- covercol.classList.add('col');
+ covercol.classList.add('col', 'd-flex', 'justify-content-center');
namecol.classList.add('col');
}
namecol.style.margin = "0 10px 0 10px";
@@ -451,11 +450,11 @@ $(document).ready(function() {
dropdownbtn.setAttribute('data-toggle', 'dropdown');
dropdownmenu.classList.add('dropdown-menu');
- tr.id = itemdata["id"];
+ tr.id = data["id"];
cover.classList.add('img-fluid', 'cover-img');
cover.setAttribute('style', 'width: 100px; height: 100px;');
- cover.src = itemdata["image"];
+ cover.src = data["image"];
form_check.appendChild(checkbox);
@@ -471,7 +470,7 @@ $(document).ready(function() {
tr.append(td_name, td_artist, td_album, td_date, td_ext, td_actions);
$("#emptyrow").remove();
$("#recordstable").children("tbody").append(tr);
- createdropdownmenu(itemdata["id"], itemdata["youtube_id"]);
+ createdropdownmenu(data["id"], data["youtube_id"]);
}
function downloadURI(uri, name) {
@@ -1450,7 +1449,7 @@ $(document).ready(function() {
})
$("#progress_status").siblings('p').empty();
filedata = {
- 'url': url,
+ 'youtube_id': $("#thumbnail_yt").attr('youtube_id'),
'ext': ext,
'output_folder': output_folder,
'output_format': output_format,
@@ -1463,11 +1462,14 @@ $(document).ready(function() {
'userMetadata': getMetadata(),
}
socket.emit('ytdl_download', filedata, function(ack) {
+ progress_text = $("#edititemmodal").css('display').toLowerCase() != 'none' ? $("#progresstextedit") : $("#progresstext");
if(ack == "OK") {
$("#editmetadata, #downloadbtn, #searchmetadataview, #404p, #defaultview, #resetviewbtn, #geniusbtn, #audiocol, #savemetadata, #metadataview, #geniuscol").addClass('d-none');
$("#progressview").removeClass('d-none');
$("#searchlog").empty();
- progress_text = $("#edititemmodal").css('display').toLowerCase() != 'none' ? $("#progresstextedit") : $("#progresstext");
+
+ } else if (ack == 'duplicate') {
+
}
});
}
@@ -1582,8 +1584,8 @@ $(document).ready(function() {
}
});
- socket.on('finished_postprocessor', function(msg) {
- if(msg.postprocessor == 'MoveFiles') {
+ socket.on('finished_postprocessor', function(postprocessor) {
+ if(postprocessor == 'MoveFiles') {
let percentage = (100 / getPhases()) * (getPhases() - 1);
setProgress(percentage);
progress_text.text('Adding metadata...');
@@ -1597,18 +1599,21 @@ $(document).ready(function() {
}
});
- socket.on('finished_metadata', function(msg) {
+ socket.on('finished_metadata', function(data) {
setProgress("100");
progress_text.text('Finished adding metadata!');
- msg.data["youtube_id"] = $("#thumbnail_yt").attr('youtube_id');
try {
- socket.emit('insertitem', msg.data);
$("#downloadfilebtn").removeClass('d-none');
- $("#downloadfilebtn").attr('filepath', msg.data["filepath"]);
+ $("#downloadfilebtn").attr('filepath', data["filepath"]);
} catch (error) {
console.error(error);
}
});
+
+ socket.on('inserted_song', function(data) {
+ $("#overviewlog").empty();
+ additem(data)
+ })
socket.on('metadata_unavailable', function(msg) {
msg.data["youtube_id"] = $("#thumbnail_yt").attr('youtube_id');
@@ -1619,7 +1624,7 @@ $(document).ready(function() {
socket.emit('insertitem', msg.data);
});
- socket.on('downloaderror', function(msg) {
+ socket.on('downloadprocesserror', function(msg) {
progress_text.text(msg);
getProgress().attr('aria-valuenow', 100);
getProgress().html('ERROR
');
diff --git a/metatube/youtube/download.py b/metatube/youtube/download.py
index 904bac2f..bb5a9ede 100644
--- a/metatube/youtube/download.py
+++ b/metatube/youtube/download.py
@@ -21,31 +21,31 @@ async def download(url: list, queue: LifoQueue, ytdl_options: dict):
return ytdl.download(url)
except KeyError as e:
logger.error('%s key did not exist', str(e))
- sockets.downloaderrors({'status': 'error', 'message': 'The output template was incorrect. Check logs for more info.'})
+ sockets.downloadprocesserror({'status': 'error', 'message': 'The output template was incorrect. Check logs for more info.'})
return None
except ExtractorError as e:
logger.error('Extractor error: %s', str(e))
- sockets.downloaderrors({'status': 'error', 'message': 'An extractor error has occured. Check logs for more info.'})
+ sockets.downloadprocesserror({'status': 'error', 'message': 'An extractor error has occured. Check logs for more info.'})
return None
except FFmpegPostProcessorError as e:
logger.error('FFmpegPostProcessor error: %s', str(e))
- sockets.downloaderrors({'status': 'error', 'message': 'An processing error involving FFmpeg has occured. Check logs for more info.'})
+ sockets.downloadprocesserror({'status': 'error', 'message': 'An processing error involving FFmpeg has occured. Check logs for more info.'})
return None
except PostProcessingError as e:
logger.error('Postprocessor error: %s', str(e))
- sockets.downloaderrors({'status': 'error', 'message': 'A processing error has occured. Check logs for more info.'})
+ sockets.downloadprocesserror({'status': 'error', 'message': 'A processing error has occured. Check logs for more info.'})
return None
except DownloadError as e:
logger.error('Downloading error: %s', str(e))
- sockets.downloaderrors({'status': 'error', 'message': 'A downloading error has occured. Check logs for more info.'})
+ sockets.downloadprocesserror({'status': 'error', 'message': 'A downloading error has occured. Check logs for more info.'})
return None
except URLError as e:
logger.error('Network connection error: %s', str(e))
- sockets.downloaderrors({'status': 'error', 'message': 'A network error occured. Check logs for more info.'})
+ sockets.downloadprocesserror({'status': 'error', 'message': 'A network error occured. Check logs for more info.'})
return None
except Exception as e:
logger.exception('Error during downloading video: %s', str(e))
- sockets.downloaderrors({'status': 'error', 'message': 'Something has gone wrong. Check logs for more info'})
+ sockets.downloadprocesserror({'status': 'error', 'message': 'Something has gone wrong. Check logs for more info'})
return None
@staticmethod
diff --git a/metatube/youtube/downloadOptions.py b/metatube/youtube/downloadOptions.py
index a7549437..db040278 100644
--- a/metatube/youtube/downloadOptions.py
+++ b/metatube/youtube/downloadOptions.py
@@ -6,6 +6,7 @@
class downloadOptions(object):
def __init__(
self,
+ youtube_id: str,
ext: str,
output_folder: str,
output_type: str,
@@ -21,6 +22,7 @@ def __init__(
verbose: bool,
):
+ self.youtube_id = youtube_id
self.ext = ext
self.output_folder = output_folder
self.output_type = output_type
@@ -35,7 +37,7 @@ def __init__(
self.height = height
self.verbose = verbose
- def optionsToDictMapper(self, metadata: MetadataObject) -> dict:
+ def downloadOptionsMapper(self, metadata: MetadataObject) -> dict:
filepath = os.path.join(self.output_folder, self.output_format)
postprocessors = []
postprocessor_args = {}
@@ -111,9 +113,11 @@ def optionsToDictMapper(self, metadata: MetadataObject) -> dict:
postprocessors.append({
'actions': [
(MetadataParserPP.interpretter, " " + metadata.title, ' %(title)s'),
+ (MetadataParserPP.interpretter, " " + metadata.title, ' %(track)s'),
(MetadataParserPP.interpretter, metadata.album, '%(album)s'),
- (MetadataParserPP.interpretter, metadata.album_artists, '%(artist)s'),
- (MetadataParserPP.interpretter, metadata.album_artists, '%(album_artist)s'),
+ (MetadataParserPP.interpretter, metadata.artists, '%(artist)s'),
+ (MetadataParserPP.interpretter, metadata.artists, '%(creator)s'),
+ (MetadataParserPP.interpretter, metadata.artists, '%(album_artist)s'),
(MetadataParserPP.interpretter, str(metadata.tracknr), '%(track_number)s'),
(MetadataParserPP.interpretter, metadata.language, '%(language)s'),
(MetadataParserPP.interpretter, metadata.genres, '%(genre)s'),
diff --git a/metatube/youtube/manageDownloadProcess.py b/metatube/youtube/manageDownloadProcess.py
index ba8662bf..7798b804 100644
--- a/metatube/youtube/manageDownloadProcess.py
+++ b/metatube/youtube/manageDownloadProcess.py
@@ -2,46 +2,57 @@
from .download import download
from metatube.metadata.processMetadata import processMetadata
from metatube.metadata.mergeMetadata import mergeMetadata
-from metatube.sockets import metadata_error, downloaderrors
+from metatube.sockets import downloadprocesserror, inserted_song, finished_metadata
from metatube.Exception import MetaTubeException
+from metatube.database import Database
from threading import Thread
from queue import Empty, LifoQueue
from time import sleep
+
class manageDownloadProcess(object):
- def __init__(self, downloadOptions: downloadOptions, metadataProcessor: processMetadata, url: str, goal: 'str'):
+ def __init__(self, downloadOptions: downloadOptions, metadataProcessor: processMetadata, goal: 'str'):
self.downloadOptions = downloadOptions
self.metadataProcessor = metadataProcessor
- self.url = url
self.goal = goal
- def start_download(self):
+ def start_download(self, app):
try:
- metadata = self.metadataProcessor.getMetadata()
+ metadata = self.metadataProcessor.getMetadata(app)
except MetaTubeException as error:
- metadata_error(str(error))
+ downloadprocesserror(str(error))
return
try:
- yt_dlpOptions = self.downloadOptions.optionsToDictMapper(metadata)
+ yt_dlpOptions = self.downloadOptions.downloadOptionsMapper(metadata)
except MetaTubeException as error:
- downloaderrors(str(error))
+ downloadprocesserror(str(error))
return
queue = LifoQueue()
- downloadProcess = Thread(target=download.start_download, args=[self.url, yt_dlpOptions, queue])
+ downloadProcess = Thread(target=download.start_download, args=[self.downloadOptions.youtube_id, yt_dlpOptions, queue])
downloadProcess.start()
downloadProcess.join()
try:
lastItem = queue.get_nowait()
filepath = lastItem['filepath']
merge = mergeMetadata(filepath, self.goal, metadata=metadata)
+ data = None
if self.downloadOptions.ext in ['MP3', 'OPUS', 'FLAC', 'OGG']:
- merge.mergeaudiodata()
+ data = merge.mergeaudiodata()
elif self.downloadOptions.ext in ['MP4', 'M4A']:
- merge.mergevideodata()
+ data = merge.mergevideodata()
elif self.downloadOptions.ext in ['WAV']:
- merge.mergeid3data()
+ data = merge.mergeid3data()
+ if type(data) is not dict:
+ downloadprocesserror("Metadata could not be merged.")
+ return
+ finished_metadata(data)
+ with app.app_context():
+ data['youtube_id'] = self.downloadOptions.youtube_id
+ id = Database.insert(data)
+ data["id"] = id
+ inserted_song(data)
except Empty:
pass
except MetaTubeException as error:
- metadata_error(str(error))
+ downloadprocesserror(str(error))
finally:
sleep(0.1)
\ No newline at end of file
From 61419e9b083f6c465e889feee87a3f5655617118 Mon Sep 17 00:00:00 2001
From: JVT038 <47184046+JVT038@users.noreply.github.com>
Date: Fri, 2 Feb 2024 18:02:26 +0100
Subject: [PATCH 03/11] Fixed changing the file data (sort of)
---
config.py | 3 +-
metatube/DatabaseExceptions.py | 3 +
metatube/database.py | 7 +-
metatube/metadata/mergeMetadata.py | 134 ++++++----------------
metatube/metadata/processMetadata.py | 120 ++++++-------------
metatube/metadata/readMetadata.py | 130 +++++++++++++++++++++
metatube/overview/routes.py | 123 ++++++++++----------
metatube/sockets.py | 3 +
metatube/static/JS/overview.js | 49 ++++----
metatube/youtube/manageDownloadProcess.py | 54 +++++++--
10 files changed, 342 insertions(+), 284 deletions(-)
create mode 100644 metatube/metadata/readMetadata.py
diff --git a/config.py b/config.py
index 35881840..7eb13aa4 100644
--- a/config.py
+++ b/config.py
@@ -26,4 +26,5 @@ class Config(object):
VIDEO_EXTENSIONS = ['MP4', 'M4A', 'FLV', 'WEBM', 'OGG', 'MKV', 'AVI']
AUDIO_EXTENSIONS = ['AAC', 'FLAC', 'MP3', 'M4A', 'OPUS', 'VORBIS', 'WAV']
INIT_DB = os.environ.get('INIT_DB', True)
- TESTING = False
\ No newline at end of file
+ TESTING = False
+ DEFAULT_COVER_PATH = os.path.join(BASE_DIR, 'metatube/static/images/empty_cover.png')
\ No newline at end of file
diff --git a/metatube/DatabaseExceptions.py b/metatube/DatabaseExceptions.py
index cb62e37c..2e9226b1 100644
--- a/metatube/DatabaseExceptions.py
+++ b/metatube/DatabaseExceptions.py
@@ -1,4 +1,7 @@
from .Exception import MetaTubeException
class InvalidItemId(MetaTubeException):
+ pass
+
+class NoDefaultTemplate(MetaTubeException):
pass
\ No newline at end of file
diff --git a/metatube/database.py b/metatube/database.py
index 94c9b9f8..03c3ec64 100644
--- a/metatube/database.py
+++ b/metatube/database.py
@@ -131,7 +131,10 @@ def delete(self):
@staticmethod
def searchdefault():
- return Templates.query.filter_by(default = True).first()
+ default = Templates.query.filter_by(default = True).first()
+ if default is None:
+ raise NoDefaultTemplate("There is no template marked as 'default'!")
+ return default
def setdefault(self, defaulttemplate = None):
self.default = True
@@ -237,7 +240,7 @@ def update(self, data):
db.session.commit()
logger.info('Updated item %s', data["name"])
data["date"] = data["date"].strftime('%d-%m-%Y')
- sockets.overview({'msg': 'changed_metadata_db', 'data': data})
+ sockets.changed_metadata(data)
def updatefilepath(self, filepath):
self.filepath = filepath
diff --git a/metatube/metadata/mergeMetadata.py b/metatube/metadata/mergeMetadata.py
index 6ed38f79..0b253272 100644
--- a/metatube/metadata/mergeMetadata.py
+++ b/metatube/metadata/mergeMetadata.py
@@ -1,6 +1,6 @@
from mutagen.id3._frames import (
# Meaning of the various frames: https://mutagen.readthedocs.io/en/latest/api/id3_frames.html
- APIC, TIT2, TALB, TCON, TLAN, TRCK, TSRC, TXXX, TPE1
+ APIC, TIT2, TALB, TCON, TLAN, TRCK, TSRC, TXXX, TPE1, USLT
)
from mutagen.id3 import ID3
from mutagen.flac import FLAC, Picture
@@ -8,6 +8,7 @@
from mutagen.wave import WAVE
from mutagen.oggopus import OggOpus
from mutagen.easyid3 import EasyID3
+from mutagen.easymp4 import EasyMP4
from mutagen.mp3 import MP3
from mutagen.oggvorbis import OggVorbis
from mutagen.mp4 import MP4, MP4Cover
@@ -20,11 +21,12 @@
class mergeMetadata():
- def __init__(self, filename: str, goal: str, metadata: MetadataObject, itemId = None):
+ def __init__(self, filename: str, goal: str, metadata: MetadataObject, youtube_id: str, itemId = None):
self.filename = filename
self.goal = goal
self.itemId = itemId
self.metadata = metadata
+ self.youtube_id = youtube_id
def mergeaudiodata(self):
'''
@@ -87,14 +89,11 @@ def mergeaudiodata(self):
'''
if self.metadata.extension == 'MP3':
audio = EasyID3(self.filename)
- if self.metadata.source == 'Spotify':
- audio.RegisterTXXXKey('spotify_songid', self.metadata.songid)
- audio.RegisterTXXXKey('spotify_albumid', self.metadata.albumid)
- elif self.metadata.source == 'Deezer':
- audio.RegisterTXXXKey('deezer_songid', self.metadata.songid)
- audio.RegisterTXXXKey('deezer_albumid', self.metadata.albumid)
- if self.metadata.lyrics is not None:
- audio.RegisterTextKey('lyrics', "USLT")
+ if self.metadata.source in ['spotify', 'deezer']:
+ audio.RegisterTXXXKey('songid', f'{self.metadata.source.upper()}_SONGID')
+ audio.RegisterTXXXKey('albumid', f'{self.metadata.source.upper()}_ALBUMID')
+ audio['songid'] = self.metadata.songid
+ audio['albumid'] = self.metadata.albumid
elif self.metadata.extension == 'FLAC':
audio = FLAC(self.filename)
elif self.metadata.extension == 'AAC':
@@ -113,8 +112,8 @@ def mergeaudiodata(self):
audio["title"] = self.metadata.title
audio["date"] = self.metadata.release_date
audio["genre"] = self.metadata.genres
- if self.metadata.lyrics is not None and self.metadata.extension != 'MP3':
- audio['lyrics'] = self.metadata.lyrics
+ audio['isrc'] = self.metadata.isrc
+
if self.metadata.source == 'Musicbrainz':
audio["musicbrainz_releasesongid"] = self.metadata.songid
audio["musicbrainz_releasegroupid"] = self.metadata.albumid
@@ -122,6 +121,11 @@ def mergeaudiodata(self):
audio.save()
+ if self.metadata.lyrics != '':
+ lyrics = ID3(self.filename)
+ lyrics.add(USLT(encoding=3, language=self.metadata.language, desc=f'Lyrics of {self.metadata.title}', text=self.metadata.lyrics))
+ lyrics.save()
+
if self.metadata.extension == 'MP3':
cover = ID3(self.filename)
cover["APIC"] = APIC(
@@ -148,10 +152,9 @@ def mergeaudiodata(self):
if self.goal == 'edit':
response["itemid"] = self.itemId
logger.info('Finished changing metadata of %s', self.metadata.title)
- sockets.overview({'msg': 'changed_metadata', 'data': response})
elif self.goal == 'add':
logger.info('Finished adding metadata to %s', self.metadata.title)
- return response
+ return response
def mergeid3data(self):
if self.metadata.extension == 'WAV':
@@ -165,25 +168,30 @@ def mergeid3data(self):
if audio.tags is None:
raise NoAudioTags("There are no metadata tags for this file.")
audio.tags.add(TIT2(encoding=3, text=self.metadata.title))
- audio.tags.add(TALB(encoding=3, text=self.metadata.album)) # type: ignore
- audio.tags.add(TCON(encoding=3, text=self.metadata.genres)) # type: ignore
- audio.tags.add(TLAN(encoding=3, text=self.metadata.language)) # type: ignore
- audio.tags.add(TRCK(encoding=3, text=self.metadata.tracknr)) # type: ignore
- audio.tags.add(TSRC(encoding=3, text=data["isrc"])) # type: ignore
- audio.tags.add(TPE1(encoding=3, text=self.metadata.artists)) # type: ignore
- audio.tags.add(TXXX(encoding=3, desc=u'musicbrainz_releasesongid', text=data["mbp_songid"])) # type: ignore
- audio.tags.add(TXXX(encoding=3, desc=u'musicbrainz_releasegroupid', text=data['mbp_albumid'])) # type: ignore
- audio.tags.add(TXXX(encoding=3, desc=u'musicbrainz_albumid', text=data["mbp_albumid"])) # type: ignore
- audio.tags.add(APIC(encoding=3, mime=self.metadata.cover_mime_type, type=3, desc=u'Cover', data=self.metadata.cover)) # type: ignore
+ audio.tags.add(TALB(encoding=3, text=self.metadata.album))
+ audio.tags.add(TCON(encoding=3, text=self.metadata.genres))
+ audio.tags.add(TLAN(encoding=3, text=self.metadata.language))
+ audio.tags.add(TRCK(encoding=3, text=self.metadata.tracknr))
+ audio.tags.add(TSRC(encoding=3, text=self.metadata.isrc))
+ audio.tags.add(TPE1(encoding=3, text=self.metadata.artists))
+ if self.metadata.source == 'musicbrainz':
+ audio.tags.add(TXXX(encoding=3, desc=u'musicbrainz_releasesongid', text=self.metadata.songid))
+ audio.tags.add(TXXX(encoding=3, desc=u'musicbrainz_albumid', text=self.metadata.albumid))
+ elif self.metadata.source == 'spotify':
+ audio.tags.add(TXXX(encoding=3, desc=u'spotify_songid', text=self.metadata.songid))
+ audio.tags.add(TXXX(encoding=3, desc=u'spotify_albumid', text=self.metadata.albumid))
+ elif self.metadata.source == 'deezer':
+ audio.tags.add(TXXX(encoding=3, desc=u'deezer_songid', text=self.metadata.songid))
+ audio.tags.add(TXXX(encoding=3, desc=u'deezer_albumid', text=self.metadata.albumid))
+ audio.tags.add(APIC(encoding=3, mime=self.metadata.cover_mime_type, type=3, desc=u'Cover', data=self.metadata.cover))
response = self.metadataResponseMapper(self.metadata.length)
if self.goal == 'edit':
response["itemid"] = self.itemId
logger.info('Finished changing metadata of %s', self.metadata.title)
- sockets.overview({'msg': 'changed_metadata', 'data': response})
elif self.goal == 'add':
logger.info('Finished adding metadata to %s', self.metadata.title)
- return response
+ return response
def mergevideodata(self):
if self.metadata.extension in ['M4A', 'MP4']:
@@ -198,6 +206,7 @@ def mergevideodata(self):
video["\xa9ART"] = self.metadata.artists
video["\xa9gen"] = self.metadata.genres
video["\xa9day"] = str(year)
+
try:
video["trkn"] = [(int(self.metadata.tracknr), int(self.metadata.total_tracks))]
except Exception:
@@ -206,85 +215,14 @@ def mergevideodata(self):
video["covr"] = [MP4Cover(self.metadata.cover, imageformat)]
video.save()
+ customTags = EasyMP4(self.filename)
response = self.metadataResponseMapper(self.metadata.length)
if self.goal == 'edit':
response["itemid"] = self.itemId
logger.info('Finished changing metadata of %s', self.metadata.title)
- sockets.overview({'msg': 'changed_metadata', 'data': response})
elif self.goal == 'add':
logger.info('Finished adding metadata to %s', self.metadata.title)
- return response
-
- @staticmethod
- def readaudiometadata(filename):
- logger.info('Reading metadata of %s', filename)
- extension = filename.split('.')[len(filename.split('.')) - 1].upper()
- if extension == 'MP3':
- audio = EasyID3(filename)
- data = MP3(filename)
- elif extension == 'FLAC':
- audio = FLAC(filename)
- data = FLAC(filename)
- elif extension == 'AAC':
- audio = AAC(filename)
- data = FLAC(filename)
- elif extension == 'OPUS':
- audio = OggOpus(filename)
- data = OggOpus(filename)
- elif extension == 'OGG':
- audio = OggVorbis(filename)
- data = OggVorbis(filename)
- else:
- raise InvalidAudioFile("The selected audio file has an invalid extension.")
-
- response = {
- 'title': (audio['title'] or [''])[0],
- 'artists': (audio['artist'] or [''])[0],
- 'album': (audio['album'] or [''])[0],
- 'barcode': (audio['barcode'] or [''])[0],
- 'genres': (audio['genre'] or [''])[0],
- 'language': (audio['language'] or [''])[0],
- 'release_date': (audio['date'] or [''])[0],
- 'album_id': "",
- 'total_tracks': "",
- 'mbp_songid': (audio['musicbrainz_releasesongid'] or [''])[0],
- 'mbp_releasegroupid': (audio['musicbrainz_releasegroupid'] or [''])[0],
- 'isrc': (audio['isrc'] or [''])[0],
- 'tracknr': (audio['tracknumber'] or [''])[0],
- 'date': (audio['date'] or [''])[0],
- 'length': data.info.length, # type: ignore
- 'bitrate': data.info.bitrate, # type: ignore
- 'output_folder': os.path.dirname(filename),
- 'filename': filename,
- "goal": "edit",
- }
-
- return response
-
- @staticmethod
- def readvideometadata(filename) -> dict | None:
- extension = filename.split('.')[len(filename.split('.')) - 1].upper()
- if extension in ['M4A', 'MP4']:
- video = MP4(filename)
- else:
- raise InvalidAudioFile("The selected video file has an invalid extension")
-
- # Bitrate calculation: https://www.reddit.com/r/headphones/comments/3xju4s/comment/cy5dn8h/?utm_source=share&utm_medium=web2x&context=3
- # Mutagen MP4 stream info: https://mutagen.readthedocs.io/en/latest/api/mp4.html#mutagen.mp4.MP4Info
- bitrate = int(video.info.bits_per_sample * video.info.sample_rate * video.info.channels)
- response = {
- 'title': (video['\xa9nam'] or [''])[0],
- 'album': (video["\xa9alb"] or [''])[0],
- 'artists': (video["\xa9ART"] or [''])[0],
- 'genres': (video["\xa9gen"] or [''])[0],
- 'release_date': (video["\xa9day"] or [''])[0],
- 'bitrate': bitrate,
- 'output_folder': os.path.dirname(filename),
- 'filename': filename,
- 'length': video.info.length,
- 'tracknr': video.get('trkn', [[1]])[0][0] # type: ignore
- }
return response
def metadataResponseMapper(self, length) -> dict:
diff --git a/metatube/metadata/processMetadata.py b/metatube/metadata/processMetadata.py
index c4769284..9c6800b7 100644
--- a/metatube/metadata/processMetadata.py
+++ b/metatube/metadata/processMetadata.py
@@ -8,6 +8,7 @@
from metatube.deezer import Deezer
from metatube.database import Config
from .metadataObject import MetadataObject
+from .readMetadata import readMetadata
from .MetadataExceptions import (
InvalidCoverURL,
NoMetadataAPIResult,
@@ -17,20 +18,10 @@
NoGeniusToken,
InvalidGeniusToken
)
-import requests, os
class processMetadata(object):
def __init__(self, usermetadata, extension):
self.usermetadata = usermetadata
- # self.title = usermetadata['title'] or None
- # self.artists = usermetadata['artists'] or None
- # self.album = usermetadata['album'] or None
- # self.date = usermetadata['release_date'] or None
- # self.albumid = usermetadata['albumid'] or None
- # self.album_artists = usermetadata['album_artists'] or None
- # self.tracknr = usermetadata['tracknr'] or None
- # self.album_releasedate = usermetadata['album_releasedate'] or None
- # self.cover = usermetadata['cover'] or None
self.songid = usermetadata['songid'] or None
self.source = usermetadata['metadata_source'] or None
self.cover = usermetadata['cover_source']
@@ -104,19 +95,10 @@ def getmusicbrainzdata(self, metadata_source) -> MetadataObject | None:
length = ""
genres = ""
cover_path = self.cover if len(self.usermetadata["cover"]) < 1 else self.usermetadata["cover"]
- magic = Magic(mime=True)
- if cover_path != os.path.join(env.BASE_DIR, 'metatube/static/images/empty_cover.png'):
- try:
- response = requests.get(cover_path)
- image = response.content
- magic = Magic(mime=True)
- cover_mime_type = magic.from_buffer(image)
- except Exception:
- raise InvalidCoverURL("Cover URL is invalid!")
- else:
- cover_mime_type = "image/png"
- file = open(cover_path, 'rb')
- image = file.read()
+ try:
+ imagedata = readMetadata.getImage(cover_path)
+ except InvalidCoverURL as e:
+ raise e from e
total_tracks = len(metadata_source["release"]["medium-list"][0]["track-list"])
@@ -156,9 +138,9 @@ def getmusicbrainzdata(self, metadata_source) -> MetadataObject | None:
mbp_albumid,
int(tracknr),
int(total_tracks),
- image,
+ imagedata['image'],
cover_path,
- cover_mime_type,
+ imagedata['mime_type'],
isrc,
'',
self.extension,
@@ -176,7 +158,6 @@ def getspotifydata(self, metadata_source) -> MetadataObject | None:
length = str(int(int(metadata_source["duration_ms"]) / 1000))
tracknr = metadata_source["track_number"] if len(self.usermetadata["album_tracknr"]) < 1 else self.usermetadata["album_tracknr"]
total_tracks = metadata_source["total_tracks"] if 'total_tracks' in metadata_source else '1'
- default_cover = os.path.join(env.BASE_DIR, 'metatube/static/images/empty_cover.png')
cover_path = metadata_source["album"]["images"][0]["url"] if len(self.usermetadata["cover"]) < 1 else self.usermetadata["cover"]
title = metadata_source["name"] if len(self.usermetadata["title"]) < 1 else self.usermetadata["title"]
genres = "" # Spotify API doesn't provide genres with tracks
@@ -184,18 +165,10 @@ def getspotifydata(self, metadata_source) -> MetadataObject | None:
for artist in metadata_source["artists"]:
spotify_artists.append(artist["name"])
artists = spotify_artists if json.loads(self.usermetadata["artists"]) == [""] else json.loads(self.usermetadata["artists"])
- if cover_path != default_cover:
- try:
- response = requests.get(cover_path)
- image = response.content
- magic = Magic(mime=True)
- cover_mime_type = magic.from_buffer(image)
- except Exception:
- raise InvalidCoverURL("Cover URL is invalid!")
- else:
- cover_mime_type = "image/png"
- file = open(cover_path, 'rb')
- image = file.read()
+ try:
+ imagedata = readMetadata.getImage(cover_path)
+ except InvalidCoverURL as e:
+ raise e from e
return MetadataObject(
title,
@@ -208,9 +181,9 @@ def getspotifydata(self, metadata_source) -> MetadataObject | None:
albumid,
int(tracknr),
int(total_tracks),
- image,
+ imagedata['image'],
cover_path,
- cover_mime_type,
+ imagedata['mime_type'],
isrc,
'',
self.extension,
@@ -227,7 +200,7 @@ def getdeezerdata(self, metadata_source) -> MetadataObject | None:
length = str(metadata_source.get('duration', '0'))
tracknr = str(metadata_source.get('track_position', 1)) if len(self.usermetadata["album_tracknr"]) < 1 else self.usermetadata["album_tracknr"]
total_tracks = 1
- default_cover = os.path.join(env.BASE_DIR, 'metatube/static/images/empty_cover.png')
+ default_cover = env.DEFAULT_COVER_PATH
cover_path = metadata_source["album"].get('cover_xl', default_cover) if len(self.usermetadata["cover"]) < 1 else self.usermetadata["cover"]
title = metadata_source["title"] if len(self.usermetadata["title"]) < 1 else self.usermetadata["title"]
deezer_artists = []
@@ -235,18 +208,10 @@ def getdeezerdata(self, metadata_source) -> MetadataObject | None:
if contributor["type"].lower() == 'artist':
deezer_artists.append(contributor["name"])
artists = deezer_artists if json.loads(self.usermetadata["artists"]) == [""] else json.loads(self.usermetadata["artists"])
- if cover_path != default_cover:
- try:
- response = requests.get(cover_path)
- image = response.content
- magic = Magic(mime=True)
- cover_mime_type = magic.from_buffer(image)
- except Exception:
- raise InvalidCoverURL("Cover URL is invalid!")
- else:
- file = open(cover_path, 'rb')
- image = file.read()
- cover_mime_type = "image/png"
+ try:
+ imagedata = readMetadata.getImage(cover_path)
+ except InvalidCoverURL as e:
+ raise e from e
return MetadataObject(
title,
@@ -259,9 +224,9 @@ def getdeezerdata(self, metadata_source) -> MetadataObject | None:
albumid,
int(tracknr),
int(total_tracks),
- image,
+ imagedata['image'],
cover_path,
- cover_mime_type,
+ imagedata['mime_type'],
isrc,
'',
self.extension,
@@ -279,25 +244,16 @@ def getgeniusdata(self, metadata_source, lyrics) -> MetadataObject | None:
language = 'Unknown'
tracknr = self.usermetadata["album_tracknr"]
total_tracks = 1
- default_cover = os.path.join(env.BASE_DIR, 'metatube/static/images/empty_cover.png')
cover_path = metadata_source["song"]["song_art_image_thumbnail_url"] if len(self.usermetadata["cover"]) < 1 else self.usermetadata["cover"]
title = metadata_source["song"]["title"] if len(self.usermetadata["title"]) < 1 else self.usermetadata["title"]
geniusartists = metadata_source["song"]["primary_artist"]["name"] + "; "
for artist in metadata_source["song"]["featured_artists"]:
geniusartists += artist["name"] + "; "
artists = geniusartists[0:len(geniusartists) - 2] if len(self.usermetadata["artists"]) < 1 else self.usermetadata["artists"]
- if cover_path != default_cover:
- try:
- response = requests.get(cover_path)
- image = response.content
- magic = Magic(mime=True)
- cover_mime_type = magic.from_buffer(image)
- except Exception:
- raise InvalidCoverURL("Cover URL is invalid!")
- else:
- cover_mime_type = "image/png"
- file = open(cover_path, 'rb')
- image = file.read()
+ try:
+ imagedata = readMetadata.getImage(cover_path)
+ except InvalidCoverURL as e:
+ raise e from e
return MetadataObject(
title,
@@ -310,9 +266,9 @@ def getgeniusdata(self, metadata_source, lyrics) -> MetadataObject | None:
albumid,
int(tracknr),
int(total_tracks),
- image,
+ imagedata['image'],
cover_path,
- cover_mime_type,
+ imagedata['mime_type'],
'',
lyrics,
self.extension,
@@ -321,20 +277,10 @@ def getgeniusdata(self, metadata_source, lyrics) -> MetadataObject | None:
)
def onlyuserdata(self) -> MetadataObject | None:
- if self.usermetadata["cover"] != '':
- try:
- cover_path = self.usermetadata["cover"]
- response = requests.get(self.usermetadata["cover"])
- image = response.content
- magic = Magic(mime=True)
- cover_mime_type = magic.from_buffer(image)
- except Exception:
- raise InvalidCoverURL("Cover URL is invalid!")
- else:
- cover_path = os.path.join(env.BASE_DIR, 'metatube/static/images/empty_cover.png')
- file = open(cover_path, 'rb')
- image = file.read()
- cover_mime_type = "image/png"
+ try:
+ imagedata = readMetadata.getImage(self.usermetadata["cover"])
+ except InvalidCoverURL as e:
+ raise e from e
return MetadataObject(
self.usermetadata.get('title', ''),
@@ -347,9 +293,9 @@ def onlyuserdata(self) -> MetadataObject | None:
self.usermetadata.get('albumid', ''),
self.usermetadata.get('album_tracknr', '1'),
self.usermetadata.get('album_tracknr', '1'),
- image,
- cover_path,
- cover_mime_type,
+ imagedata['image'],
+ self.usermetadata["cover"],
+ imagedata['mime_type'],
'',
'',
self.extension,
diff --git a/metatube/metadata/readMetadata.py b/metatube/metadata/readMetadata.py
new file mode 100644
index 00000000..24c96a83
--- /dev/null
+++ b/metatube/metadata/readMetadata.py
@@ -0,0 +1,130 @@
+from mutagen.flac import FLAC
+from mutagen.aac import AAC
+from mutagen.oggopus import OggOpus
+from mutagen.easyid3 import EasyID3
+from mutagen.oggvorbis import OggVorbis
+from mutagen.mp4 import MP4
+from magic import Magic
+from metatube import logger
+from config import Config as env
+from .MetadataExceptions import *
+from .metadataObject import MetadataObject
+import requests
+
+class readMetadata(object):
+ @staticmethod
+ def getImage(cover_path) -> dict:
+ magic = Magic(mime=True)
+ if cover_path != env.DEFAULT_COVER_PATH and cover_path != '':
+ try:
+ response = requests.get(cover_path)
+ image = response.content
+ magic = Magic(mime=True)
+ cover_mime_type = magic.from_buffer(image)
+ return {
+ 'mime_type' : cover_mime_type,
+ 'image': image
+ }
+ except Exception:
+ raise InvalidCoverURL("Cover URL is invalid!")
+ else:
+ file = open(env.DEFAULT_COVER_PATH, 'rb')
+ return {
+ 'mime_type' : 'image/png',
+ 'image': file.read()
+ }
+
+ @staticmethod
+ def readAudioMetadata(filename, songid, cover_path) -> MetadataObject:
+ logger.info('Reading metadata of %s', filename)
+ extension = filename.split('.')[len(filename.split('.')) - 1].upper()
+ if extension == 'MP3':
+ audio = EasyID3(filename)
+ elif extension == 'FLAC':
+ audio = FLAC(filename)
+ elif extension == 'AAC':
+ audio = AAC(filename)
+ elif extension == 'OPUS':
+ audio = OggOpus(filename)
+ elif extension == 'OGG':
+ audio = OggVorbis(filename)
+ else:
+ raise InvalidAudioFile("The selected audio file has an invalid extension.")
+
+ coverdata = readMetadata.getImage(cover_path)
+ source = ''
+ songid = ''
+ albumid = ''
+
+ if 'SPOTIFY_SONGID' in audio:
+ source = 'spotify'
+ elif 'DEEZER_SONGID' in audio:
+ source = 'deezer'
+ elif 'musicbrainz_releasesongid' in audio:
+ source = 'musicbrainz'
+
+ if songid == '':
+ songid = audio.get('SPOTIFY_SONGID', [''])[0] # type: ignore
+ if songid == '':
+ songid = audio.get('DEEZER_SONGID', [''])[0] # type: ignore
+ if songid == '':
+ songid = audio.get('musicbrainz_releasesongid', [''])[0] # type: ignore
+
+ if albumid == '':
+ albumid = audio.get('SPOTIFY_ALBUMID', [''])[0] # type: ignore
+ if albumid == '':
+ albumid = audio.get('DEEZER_ALBUMID', [''])[0] # type: ignore
+ if albumid == '':
+ albumid = audio.get('musicbrainz_releasealbumid', [''])[0] # type: ignore
+
+ return MetadataObject(
+ audio.get('title', [''])[0], # type: ignore
+ audio.get('artist', [''])[0], # type: ignore
+ audio.get('album', [''])[0], # type: ignore
+ audio.get('genre', [''])[0], # type: ignore
+ audio.get('language', [''])[0], # type: ignore
+ audio.get('date', [''])[0], # type: ignore
+ songid,
+ albumid,
+ audio.get('tracknumber', [''])[0], # type: ignore
+ 1,
+ coverdata['image'],
+ cover_path,
+ coverdata['mime_type'],
+ audio.get('isrc', [''])[0], # type: ignore
+ audio.get('lyrics', [''])[0], # type: ignore
+ extension,
+ "",
+ source
+ )
+
+ @staticmethod
+ def readVideoMetadata(filename, songid, cover_path) -> MetadataObject:
+ extension = filename.split('.')[len(filename.split('.')) - 1].upper()
+ if extension in ['M4A', 'MP4']:
+ video = MP4(filename)
+ else:
+ raise InvalidAudioFile("The selected video file has an invalid extension")
+
+ coverdata = readMetadata.getImage(cover_path)
+
+ return MetadataObject(
+ video.get('\xa9nam', [''])[0], # type: ignore
+ video.get("\xa9ART", [''])[0], # type: ignore
+ video.get("\xa9alb", [''])[0], # type: ignore
+ video.get("\xa9gen", [''])[0], # type: ignore
+ "Unknown",
+ video.get("\xa9day", [''])[0], # type: ignore
+ songid,
+ '',
+ 1,
+ 1,
+ coverdata['image'],
+ cover_path,
+ coverdata['mime_type'],
+ "Unknown",
+ "",
+ extension,
+ "",
+ ''
+ )
\ No newline at end of file
diff --git a/metatube/overview/routes.py b/metatube/overview/routes.py
index ab098e5d..bc4d8420 100644
--- a/metatube/overview/routes.py
+++ b/metatube/overview/routes.py
@@ -36,7 +36,7 @@ def index():
return render_template('overview.html', current_page='overview', ffmpeg_path=ffmpeg_path, records=records, metadataview=metadataform, genius=genius)
@socketio.on('searchitem')
-def searchitem(query):
+def handle_searchitem(query):
items = Database.searchrecords(query)
list = []
for itemdata in items:
@@ -113,12 +113,19 @@ def searchmetadata(data):
@socketio.on('ytdl_download')
def download(data):
- if Database.songidexists(data['userMetadata']['songid']) is True:
- sockets.downloadprocesserror("This song has already been downloaded.")
- return
ext = str(data["ext"]).upper() or 'MP3'
-
- processedMetadata = processMetadata(data['userMetadata'], ext)
+ item = None
+ processedMetadata = None
+ if data['goal'] == 'add':
+ if Database.songidexists(data['userMetadata']['songid'] or -1) is True:
+ sockets.downloadprocesserror("This song has already been downloaded.")
+ return
+ processedMetadata = processMetadata(data['userMetadata'], ext)
+ elif data['goal'] == 'edit':
+ item = Database.fetchitem(data['itemid'] or -1)
+ if isinstance(item, Database) == False:
+ return
+
output_folder = data["output_folder"] or '/downloads'
output_type = data["type"] or 'Audio'
output_format = data["output_format"] or f'%(title)s.%(ext)s'
@@ -149,11 +156,9 @@ def download(data):
height,
verbose
)
- if isinstance(ytdl_options, downloadOptions):
- logger.info('Request to download %s', data["youtube_id"])
- downloadProcess = manageDownloadProcess(ytdl_options, processedMetadata, 'add')
-
- socketio.start_background_task(downloadProcess.start_download, current_app._get_current_object()) # type: ignore
+ downloadProcess = manageDownloadProcess(ytdl_options, processedMetadata, data['goal'], item)
+ socketio.start_background_task(downloadProcess.start_download, current_app._get_current_object()) # type: ignore
+ logger.info('Request to download %s', data["youtube_id"])
return 'OK'
@socketio.on('fetchmbprelease')
@@ -221,9 +226,9 @@ def updateitem(data):
@socketio.on('downloaditems')
def downloaditems(items):
+ tmpdir = mkdtemp()
try:
output_string = ''.join(random.SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(5))
- tmpdir = mkdtemp()
zipfilename = "items_" + output_string + ".zip"
zipfilepath = os.path.join(tmpdir, zipfilename)
zipfile = ZipFile(zipfilepath, 'w')
@@ -407,7 +412,7 @@ def editmetadata(id):
if extension in ['MP3', 'OPUS', 'FLAC', 'OGG']:
metadata = MetaData.readaudiometadata(item.filepath)
elif extension in ["M4A", 'MP4']:
- metadata = MetaData.readvideometadata(item.filepath)
+ metadata = MetaData.readVideoMetadata(item.filepath)
else:
return False
metadata["songid"] = item.songid
@@ -439,55 +444,55 @@ def editfile(id):
downloadform = render_template('downloadform.html', templates=templates, segments=segments, default=defaulttemplate)
sockets.editfile({'data': itemdata, 'downloadview': downloadform})
-@socketio.on('editfilerequest')
-def editfilerequest(filepath, id):
- item = Database.fetchitem(id)
- if item is not None:
- extension = item.filepath.split('.')[len(item.filepath.split('.')) - 1].upper()
- new_extension = filepath.split('.')[len(item.filepath.split('.')) - 1].upper()
- if item.cover != os.path.join(env.BASE_DIR, 'metatube/static/images/empty_cover.png'):
- try:
- response = requests.get(item.cover)
- image = response.content
- magic = Magic(mime=True)
- mime_type = magic.from_buffer(image)
- except Exception:
- sockets.downloadprocesserror('Cover URL is invalid!')
- return False
- else:
- file = open(item.cover, 'rb')
- image = file.read()
- mime_type = 'image/png'
- if extension in ['MP3', 'OPUS', 'FLAC', 'OGG']:
- metadata_item = MetaData.readaudiometadata(item.filepath)
+# @socketio.on('editfilerequest')
+# def editfilerequest(filepath, id):
+# item = Database.fetchitem(id)
+# if item is not None:
+# extension = item.filepath.split('.')[len(item.filepath.split('.')) - 1].upper()
+# new_extension = filepath.split('.')[len(item.filepath.split('.')) - 1].upper()
+# if item.cover != env.DEFAULT_COVER_PATH:
+# try:
+# response = requests.get(item.cover)
+# image = response.content
+# magic = Magic(mime=True)
+# mime_type = magic.from_buffer(image)
+# except Exception:
+# sockets.downloadprocesserror('Cover URL is invalid!')
+# return False
+# else:
+# file = open(item.cover, 'rb')
+# image = file.read()
+# mime_type = 'image/png'
+# if extension in ['MP3', 'OPUS', 'FLAC', 'OGG']:
+# metadata_item = MetaData.readaudiometadata(item.filepath)
- elif extension in ['MP4', 'M4A']:
- metadata_item = MetaData.readvideometadata(item.filepath)
- metadata_item["barcode"] = ""
- metadata_item["language"] = ""
+# elif extension in ['MP4', 'M4A']:
+# metadata_item = MetaData.readVideoMetadata(item.filepath)
+# metadata_item["barcode"] = ""
+# metadata_item["language"] = ""
- metadata_item["songid"] = item.songid
- metadata_item["cover_path"] = item.cover
- metadata_item["cover_mime_type"] = mime_type
- metadata_item["image"] = image
- metadata_item["itemid"] = item.id
- metadata_item["goal"] = 'edit'
- metadata_item["extension"] = new_extension
- metadata_item["filename"] = filepath
+# metadata_item["songid"] = item.songid
+# metadata_item["cover_path"] = item.cover
+# metadata_item["cover_mime_type"] = mime_type
+# metadata_item["image"] = image
+# metadata_item["itemid"] = item.id
+# metadata_item["goal"] = 'edit'
+# metadata_item["extension"] = new_extension
+# metadata_item["filename"] = filepath
- if new_extension in ['MP3', 'OPUS', 'FLAC', 'OGG']:
- MetaData.mergeaudiodata(metadata_item)
- elif new_extension in ['MP4', 'M4A']:
- MetaData.mergevideodata(metadata_item)
- head, tail = os.path.split(filepath)
- move(filepath, os.path.join(head, tail[4:len(tail)]))
- try:
- os.unlink(item.filepath)
- except Exception:
- pass
- logger.info('Edited file %s', tail)
- else:
- logger.info('File not in database')
+# if new_extension in ['MP3', 'OPUS', 'FLAC', 'OGG']:
+# MetaData.mergeaudiodata(metadata_item)
+# elif new_extension in ['MP4', 'M4A']:
+# MetaData.mergevideodata(metadata_item)
+# head, tail = os.path.split(filepath)
+# move(filepath, os.path.join(head, tail[4:len(tail)]))
+# try:
+# os.unlink(item.filepath)
+# except Exception:
+# pass
+# logger.info('Edited file %s', tail)
+# else:
+# logger.info('File not in database')
@socketio.on('editmetadatarequest')
def editmetadatarequest(metadata_user, filepath, id):
diff --git a/metatube/sockets.py b/metatube/sockets.py
index d3b83f0e..fc7956b6 100644
--- a/metatube/sockets.py
+++ b/metatube/sockets.py
@@ -85,5 +85,8 @@ def finished_download():
def finished_metadata(data):
socketio.emit('finished_metadata', data)
+def changed_metadata(data):
+ socketio.emit('changed_metadata', data)
+
def inserted_song(data):
socketio.emit('inserted_song', data)
\ No newline at end of file
diff --git a/metatube/static/JS/overview.js b/metatube/static/JS/overview.js
index 070fd3cd..91bc2be1 100644
--- a/metatube/static/JS/overview.js
+++ b/metatube/static/JS/overview.js
@@ -701,7 +701,7 @@ $(document).ready(function() {
function getMetadata() {
let songid = $(".audiocol-checkbox:checked").parent().parent().attr('id');
- let albumid;
+ let albumid = '';
let people = {};
let metadata_source = $("#audiocol").length > 0 ? $(".audiocol-checkbox:checked").parents('li').find('span.metadatasource').text() : "Unavailable";
let cover_source = $("#audiocol").length > 0 ? $(".audiocol-checkbox:checked").parents('li').children('img').attr('src') : "Unavailable";
@@ -710,16 +710,11 @@ $(document).ready(function() {
songid = $("#spotify_songid").length > 0 ? $("#spotify_songid").val() : ($("#deezer_songid").val().length > 0 ? $("#deezer_songid").val() : $("#mbp_songid").val());
albumid = $("#spotify_albumid").length > 0 ? $("#spotify_albumid").val() : ($("#deezer_albumid").val().length > 0 ? $("#deezer_albumid").val() : $("#mbp_albumid").val());
} else if(metadata_source == 'Spotify') {
-
albumid = $("#spotify_albumid").val();
} else if(metadata_source == 'Musicbrainz') {
-
albumid = $("#mbp_albumid").val();
- } else if(metadata_source == 'Deezer') {
-
+ } else if(metadata_source == 'Deezer') {
albumid = $("#deezer_albumid").val();
- } else if(metadata_source == 'Genius') {
-
}
$.each($('.artist_relations'), function() {
@@ -1105,8 +1100,7 @@ $(document).ready(function() {
'proxy_password': $("#proxy_type").val() == 'None' ? '' : $("#proxy_password").val(),
})
$("#progress_status").siblings('p').empty();
- data = {
- 'url': url,
+ filedata = {
'ext': ext,
'output_folder': output_folder,
'output_format': output_format,
@@ -1115,9 +1109,12 @@ $(document).ready(function() {
'skipfragments': skipfragments,
'proxy_data': proxy_data,
'width': width,
- 'height': height
+ 'height': height,
+ 'goal': 'edit',
+ 'itemid': document.getElementById('edititemmodal').dataset.itemid,
+ 'youtube_id': document.getElementById('edititemmodal').dataset.youtube_id,
}
- socket.emit('ytdl_download', data, function(ack) {
+ socket.emit('ytdl_download', filedata, function(ack) {
if(ack == "OK") {
$("#metadatasection, #downloadsection, #editfilebtnmodal").addClass('d-none');
$("#progressection").removeClass('d-none');
@@ -1459,6 +1456,7 @@ $(document).ready(function() {
'proxy_data': proxy_data,
'width': width,
'height': height,
+ 'goal': 'add',
'userMetadata': getMetadata(),
}
socket.emit('ytdl_download', filedata, function(ack) {
@@ -1467,9 +1465,6 @@ $(document).ready(function() {
$("#editmetadata, #downloadbtn, #searchmetadataview, #404p, #defaultview, #resetviewbtn, #geniusbtn, #audiocol, #savemetadata, #metadataview, #geniuscol").addClass('d-none');
$("#progressview").removeClass('d-none');
$("#searchlog").empty();
-
- } else if (ack == 'duplicate') {
-
}
});
}
@@ -1610,6 +1605,18 @@ $(document).ready(function() {
}
});
+ socket.on('changed_metadata', function(data) {
+ let tr = $("tr#"+data.itemid);
+ tr.find('img').attr('src', data.image);
+ tr.find('img').siblings('span').text(data.name);
+ tr.find('.td_artist').text(data.artist);
+ tr.find('.td_album').text(data.album);
+ tr.find('.td_date').text(data.date);
+ tr.find('.td_filepath').text(data.filepath.split('.')[data.filepath.split('.').length - 1]);
+ $("#overviewlog").text("Item metadata has been changed!");
+ $("#edititemmodal").modal('hide');
+ });
+
socket.on('inserted_song', function(data) {
$("#overviewlog").empty();
additem(data)
@@ -1920,18 +1927,6 @@ $(document).ready(function() {
ap.play();
$("#recordstable").parent().css('height', '65vh');
$("#audioplayer").removeClass('d-none')
- } else if(data.msg == 'changed_metadata') {
- socket.emit('updateitem', data.data);
- } else if(data.msg == 'changed_metadata_db') {
- let tr = $("tr#"+data.data.itemid);
- tr.find('img').attr('src', data.data.image);
- tr.find('img').siblings('span').text(data.data.name);
- tr.find('.td_artist').text(data.data.artist);
- tr.find('.td_album').text(data.data.album);
- tr.find('.td_date').text(data.data.date);
- tr.find('.td_filepath').text(data.data.filepath.split('.')[data.data.filepath.split('.').length - 1]);
- $("#overviewlog").text("Item metadata has been changed!");
- $("#edititemmodal").modal('hide');
} else if(data.msg == 'deleteitems') {
$(".selectitem:checked").parents('tr').remove();
$("#bulkactionsrow").css('visibility', 'hidden');
@@ -2022,7 +2017,7 @@ $(document).ready(function() {
$("#editmetadatabtnmodal").attr('id', 'editfilebtnmodal');
$("#downloadsection, #editfilebtnmodal").removeClass('d-none');
$("#downloadsection").find('hr').remove();
- $("#edititemmodal").attr({'itemid': data.filedata.itemid, 'youtube_id': data.filedata.youtube_id});
+ $("#edititemmodal").attr({'data-itemid': data.data.itemid, 'data-youtube_id': data.data.youtube_id});
$("hr").addClass('d-none');
$("#edititemmodal").addClass(['d-flex', 'justify-content-center']);
diff --git a/metatube/youtube/manageDownloadProcess.py b/metatube/youtube/manageDownloadProcess.py
index 7798b804..6df56660 100644
--- a/metatube/youtube/manageDownloadProcess.py
+++ b/metatube/youtube/manageDownloadProcess.py
@@ -2,24 +2,46 @@
from .download import download
from metatube.metadata.processMetadata import processMetadata
from metatube.metadata.mergeMetadata import mergeMetadata
-from metatube.sockets import downloadprocesserror, inserted_song, finished_metadata
+from metatube.metadata.readMetadata import readMetadata
+from metatube.sockets import downloadprocesserror, inserted_song, finished_metadata, changed_metadata
from metatube.Exception import MetaTubeException
from metatube.database import Database
from threading import Thread
from queue import Empty, LifoQueue
from time import sleep
+import os
+from dateutil import parser
+from datetime import datetime
class manageDownloadProcess(object):
- def __init__(self, downloadOptions: downloadOptions, metadataProcessor: processMetadata, goal: 'str'):
+ def __init__(self, downloadOptions: downloadOptions, metadataProcessor: processMetadata | None, goal: 'str', item: Database | None):
self.downloadOptions = downloadOptions
self.metadataProcessor = metadataProcessor
self.goal = goal
+ self.item = item
def start_download(self, app):
- try:
- metadata = self.metadataProcessor.getMetadata(app)
- except MetaTubeException as error:
- downloadprocesserror(str(error))
+ metadata = None
+ if self.goal == 'add' and self.metadataProcessor is not None:
+ try:
+ metadata = self.metadataProcessor.getMetadata(app)
+ except MetaTubeException as error:
+ downloadprocesserror(str(error))
+ return
+ elif self.goal == 'edit':
+ if self.item is not None:
+ try:
+ if self.downloadOptions.ext in ['MP3', 'FLAC', 'AAC', 'OPUS', 'OGG']:
+ metadata = readMetadata.readAudioMetadata(self.item.filepath, self.item.songid, self.item.cover)
+ elif self.downloadOptions.ext in ['MP4', 'M4A']:
+ metadata = readMetadata.readVideoMetadata(self.item.filepath, self.item.songid, self.item.cover)
+ except MetaTubeException as error:
+ downloadprocesserror(str(error))
+ return
+ else:
+ return
+ if metadata is None:
+ downloadprocesserror("Download process could not be started. Please check thet logs and try again.")
return
try:
yt_dlpOptions = self.downloadOptions.downloadOptionsMapper(metadata)
@@ -33,7 +55,7 @@ def start_download(self, app):
try:
lastItem = queue.get_nowait()
filepath = lastItem['filepath']
- merge = mergeMetadata(filepath, self.goal, metadata=metadata)
+ merge = mergeMetadata(filepath, self.goal, metadata, youtube_id=self.downloadOptions.youtube_id)
data = None
if self.downloadOptions.ext in ['MP3', 'OPUS', 'FLAC', 'OGG']:
data = merge.mergeaudiodata()
@@ -47,9 +69,21 @@ def start_download(self, app):
finished_metadata(data)
with app.app_context():
data['youtube_id'] = self.downloadOptions.youtube_id
- id = Database.insert(data)
- data["id"] = id
- inserted_song(data)
+ if self.goal == 'add':
+ id = Database.insert(data)
+ data["id"] = id
+ inserted_song(data)
+ elif self.goal == 'edit':
+ changed_metadata(data)
+ id = data["itemid"]
+ head, tail = os.path.split(data["filepath"])
+ if tail.startswith('tmp_'):
+ data["filepath"] = os.path.join(head, tail[4:len(tail)])
+ try:
+ data["date"] = parser.parse(data["date"])
+ except Exception:
+ data["date"] = datetime.now().date()
+ self.item.update(data) # type: ignore
except Empty:
pass
except MetaTubeException as error:
From 84d4eab0da2458cdd42cc9daa833cd282a223882 Mon Sep 17 00:00:00 2001
From: JVT038 <47184046+JVT038@users.noreply.github.com>
Date: Fri, 2 Feb 2024 22:03:25 +0100
Subject: [PATCH 04/11] Overhauled the edit file and edit metadata stuff
---
README.md | 1 +
metatube/database.py | 3 -
metatube/metadata/mergeMetadata.py | 15 +-
metatube/metadata/metadataObject.py | 26 ++-
metatube/metadata/processMetadata.py | 20 --
metatube/metadata/readMetadata.py | 4 -
metatube/overview/routes.py | 123 +++++-------
metatube/sockets.py | 3 +
metatube/spotify.py | 2 +-
metatube/static/JS/overview.js | 174 ++++++++++-------
metatube/templates/metadataform.html | 28 ++-
metatube/templates/modals/addtemplate.html | 139 +++++++++++++
metatube/templates/modals/download.html | 72 +++++++
metatube/templates/modals/editfile.html | 33 ++++
metatube/templates/modals/editmetadata.html | 45 +++++
metatube/templates/modals/filebrowser.html | 56 ++++++
metatube/templates/modals/removeitem.html | 17 ++
metatube/templates/modals/removetemplate.html | 19 ++
metatube/templates/overview.html | 184 +-----------------
metatube/templates/settings.html | 160 +--------------
metatube/youtube/manageDownloadProcess.py | 15 +-
metatube/youtube/youtubeUtils.py | 3 +-
tests/testDatabase.py | 2 +-
23 files changed, 606 insertions(+), 538 deletions(-)
create mode 100644 metatube/templates/modals/addtemplate.html
create mode 100644 metatube/templates/modals/download.html
create mode 100644 metatube/templates/modals/editfile.html
create mode 100644 metatube/templates/modals/editmetadata.html
create mode 100644 metatube/templates/modals/filebrowser.html
create mode 100644 metatube/templates/modals/removeitem.html
create mode 100644 metatube/templates/modals/removetemplate.html
diff --git a/README.md b/README.md
index 5caca397..8d847c1e 100644
--- a/README.md
+++ b/README.md
@@ -286,6 +286,7 @@ Made with :heart: by
JVT038<
- [ ] Send websocket requests to one specific device / client only, to prevent duplicate websocket requests
- [ ] Add unit tests for the download, metadata logic, template / database stuff, config detection, automatic migrations *
- [ ] Develop a plugin for yt-dlp to merge the metadata and handle it with postprocessors instead of a custom class
+- [ ] Perform bulk actions for multiple items (such as changing the output template or the directory)
\* in progress
diff --git a/metatube/database.py b/metatube/database.py
index 03c3ec64..ccb2a191 100644
--- a/metatube/database.py
+++ b/metatube/database.py
@@ -169,7 +169,6 @@ class Database(db.Model):
artist = db.Column(db.String(64))
album = db.Column(db.String(64))
date = db.Column(db.DateTime)
- length = db.Column(db.Integer)
cover = db.Column(db.String(256))
songid = db.Column(db.String(128))
youtube_id = db.Column(db.String(16), unique=True)
@@ -233,14 +232,12 @@ def update(self, data):
self.artist = data["artist"]
self.album = data["album"]
self.date = data["date"]
- self.length = data["length"]
self.cover = data["image"]
self.songid = data["songid"]
self.youtube_id = data["youtube_id"]
db.session.commit()
logger.info('Updated item %s', data["name"])
data["date"] = data["date"].strftime('%d-%m-%Y')
- sockets.changed_metadata(data)
def updatefilepath(self, filepath):
self.filepath = filepath
diff --git a/metatube/metadata/mergeMetadata.py b/metatube/metadata/mergeMetadata.py
index 0b253272..7a13d077 100644
--- a/metatube/metadata/mergeMetadata.py
+++ b/metatube/metadata/mergeMetadata.py
@@ -148,7 +148,7 @@ def mergeaudiodata(self):
cover_data = cover.write()
audio["metadata_block_picture"] = [base64.b64encode(cover_data).decode('ascii')]
audio.save()
- response = self.metadataResponseMapper(self.metadata.length)
+ response = self.metadataResponseMapper()
if self.goal == 'edit':
response["itemid"] = self.itemId
logger.info('Finished changing metadata of %s', self.metadata.title)
@@ -185,7 +185,7 @@ def mergeid3data(self):
audio.tags.add(TXXX(encoding=3, desc=u'deezer_albumid', text=self.metadata.albumid))
audio.tags.add(APIC(encoding=3, mime=self.metadata.cover_mime_type, type=3, desc=u'Cover', data=self.metadata.cover))
- response = self.metadataResponseMapper(self.metadata.length)
+ response = self.metadataResponseMapper()
if self.goal == 'edit':
response["itemid"] = self.itemId
logger.info('Finished changing metadata of %s', self.metadata.title)
@@ -206,17 +206,13 @@ def mergevideodata(self):
video["\xa9ART"] = self.metadata.artists
video["\xa9gen"] = self.metadata.genres
video["\xa9day"] = str(year)
-
- try:
- video["trkn"] = [(int(self.metadata.tracknr), int(self.metadata.total_tracks))]
- except Exception:
- pass
+
imageformat = MP4Cover.FORMAT_PNG if "png" in self.metadata.cover_mime_type else MP4Cover.FORMAT_JPEG
video["covr"] = [MP4Cover(self.metadata.cover, imageformat)]
video.save()
customTags = EasyMP4(self.filename)
- response = self.metadataResponseMapper(self.metadata.length)
+ response = self.metadataResponseMapper()
if self.goal == 'edit':
response["itemid"] = self.itemId
@@ -225,14 +221,13 @@ def mergevideodata(self):
logger.info('Finished adding metadata to %s', self.metadata.title)
return response
- def metadataResponseMapper(self, length) -> dict:
+ def metadataResponseMapper(self) -> dict:
return {
'filepath': os.path.join(Config.BASE_DIR, self.filename),
'name': self.metadata.title,
'artist': self.metadata.artists,
'album': self.metadata.album,
'date': self.metadata.release_date,
- 'length': length,
'image': self.metadata.cover_path,
'songid': self.metadata.songid
}
diff --git a/metatube/metadata/metadataObject.py b/metatube/metadata/metadataObject.py
index edc3c3ea..6af1a866 100644
--- a/metatube/metadata/metadataObject.py
+++ b/metatube/metadata/metadataObject.py
@@ -11,14 +11,12 @@ def __init__(
songid: str | None,
albumid: str | None,
tracknr: int | None,
- total_tracks: int | None,
cover: bytes,
cover_path: str | None,
cover_mime_type: str | None,
isrc: str | None,
lyrics: str | None,
extension: str | None,
- length: str | None,
source: str | None,
):
self.title = title or ''
@@ -30,12 +28,30 @@ def __init__(
self.albumid = albumid or ''
self.songid = songid or ''
self.tracknr = tracknr or 1
- self.total_tracks = total_tracks or 1
self.cover = cover
self.cover_path = cover_path or ''
self.cover_mime_type = cover_mime_type or ''
self.isrc = isrc or ''
self.lyrics = lyrics or ''
self.extension = extension or ''
- self.length = length or ''
- self.source = source or ''
\ No newline at end of file
+ self.source = source or ''
+
+ def metadataMapper(self):
+ return {
+ 'title': self.title,
+ 'artists': self.artists,
+ 'album': self.album,
+ 'genres': self.genres,
+ 'language': self.language,
+ 'release_date': self.release_date,
+ 'songid': self.songid,
+ 'albumid': self.albumid,
+ 'tracknr': self.tracknr,
+ 'cover': self.cover,
+ 'cover_path': self.cover_path,
+ 'cover_mime_type': self.cover_mime_type,
+ 'isrc': self.isrc,
+ 'lyrics': self.lyrics,
+ 'extension': self.extension,
+ 'source': self.source
+ }
\ No newline at end of file
diff --git a/metatube/metadata/processMetadata.py b/metatube/metadata/processMetadata.py
index 9c6800b7..be99a8ee 100644
--- a/metatube/metadata/processMetadata.py
+++ b/metatube/metadata/processMetadata.py
@@ -1,5 +1,4 @@
import json
-from magic import Magic
from metatube.spotify import spotify_metadata as Spotify
from metatube.genius import Genius
from re import M
@@ -92,7 +91,6 @@ def getmusicbrainzdata(self, metadata_source) -> MetadataObject | None:
mbp_songid = ""
tracknr = ""
isrc = ""
- length = ""
genres = ""
cover_path = self.cover if len(self.usermetadata["cover"]) < 1 else self.usermetadata["cover"]
try:
@@ -100,14 +98,11 @@ def getmusicbrainzdata(self, metadata_source) -> MetadataObject | None:
except InvalidCoverURL as e:
raise e from e
- total_tracks = len(metadata_source["release"]["medium-list"][0]["track-list"])
-
for track in metadata_source["release"]["medium-list"][0]["track-list"]:
if metadata_source["release"]["title"] in track["recording"]["title"]:
tracknr += track["number"] if "number" in track and len(track["number"]) > 0 else 1 # type: ignore
mbp_songid += track["id"]
isrc += track["recording"]["isrc-list"][0] if "isrc-list" in track["recording"] else ''
- length += track["recording"]["length"] if "length" in track["recording"] else ''
genres = ""
try:
if "tag-list" in metadata_source["release"]["release-group"]:
@@ -137,14 +132,12 @@ def getmusicbrainzdata(self, metadata_source) -> MetadataObject | None:
mbp_songid,
mbp_albumid,
int(tracknr),
- int(total_tracks),
imagedata['image'],
cover_path,
imagedata['mime_type'],
isrc,
'',
self.extension,
- length,
'musicbrainz'
)
@@ -155,9 +148,7 @@ def getspotifydata(self, metadata_source) -> MetadataObject | None:
albumid = metadata_source["album"]["id"] if len(self.usermetadata["albumid"]) < 1 else self.usermetadata["albumid"]
isrc = metadata_source["external_ids"].get('isrc', '')
release_date = metadata_source["album"]["release_date"] if len(self.usermetadata["album_releasedate"]) < 1 else self.usermetadata["album_releasedate"]
- length = str(int(int(metadata_source["duration_ms"]) / 1000))
tracknr = metadata_source["track_number"] if len(self.usermetadata["album_tracknr"]) < 1 else self.usermetadata["album_tracknr"]
- total_tracks = metadata_source["total_tracks"] if 'total_tracks' in metadata_source else '1'
cover_path = metadata_source["album"]["images"][0]["url"] if len(self.usermetadata["cover"]) < 1 else self.usermetadata["cover"]
title = metadata_source["name"] if len(self.usermetadata["title"]) < 1 else self.usermetadata["title"]
genres = "" # Spotify API doesn't provide genres with tracks
@@ -180,14 +171,12 @@ def getspotifydata(self, metadata_source) -> MetadataObject | None:
songid,
albumid,
int(tracknr),
- int(total_tracks),
imagedata['image'],
cover_path,
imagedata['mime_type'],
isrc,
'',
self.extension,
- length,
'spotify',
)
@@ -197,9 +186,7 @@ def getdeezerdata(self, metadata_source) -> MetadataObject | None:
albumid = str(metadata_source["album"]["id"]) if len(self.usermetadata["albumid"]) < 1 else str(self.usermetadata["albumid"])
isrc = metadata_source.get('isrc', '')
release_date = metadata_source["release_date"] if len(self.usermetadata["album_releasedate"]) < 1 else self.usermetadata["album_releasedate"]
- length = str(metadata_source.get('duration', '0'))
tracknr = str(metadata_source.get('track_position', 1)) if len(self.usermetadata["album_tracknr"]) < 1 else self.usermetadata["album_tracknr"]
- total_tracks = 1
default_cover = env.DEFAULT_COVER_PATH
cover_path = metadata_source["album"].get('cover_xl', default_cover) if len(self.usermetadata["cover"]) < 1 else self.usermetadata["cover"]
title = metadata_source["title"] if len(self.usermetadata["title"]) < 1 else self.usermetadata["title"]
@@ -223,14 +210,12 @@ def getdeezerdata(self, metadata_source) -> MetadataObject | None:
songid,
albumid,
int(tracknr),
- int(total_tracks),
imagedata['image'],
cover_path,
imagedata['mime_type'],
isrc,
'',
self.extension,
- length,
'deezer',
)
@@ -243,7 +228,6 @@ def getgeniusdata(self, metadata_source, lyrics) -> MetadataObject | None:
genres = ''
language = 'Unknown'
tracknr = self.usermetadata["album_tracknr"]
- total_tracks = 1
cover_path = metadata_source["song"]["song_art_image_thumbnail_url"] if len(self.usermetadata["cover"]) < 1 else self.usermetadata["cover"]
title = metadata_source["song"]["title"] if len(self.usermetadata["title"]) < 1 else self.usermetadata["title"]
geniusartists = metadata_source["song"]["primary_artist"]["name"] + "; "
@@ -265,14 +249,12 @@ def getgeniusdata(self, metadata_source, lyrics) -> MetadataObject | None:
songid,
albumid,
int(tracknr),
- int(total_tracks),
imagedata['image'],
cover_path,
imagedata['mime_type'],
'',
lyrics,
self.extension,
- '',
'genius',
)
@@ -292,13 +274,11 @@ def onlyuserdata(self) -> MetadataObject | None:
self.usermetadata.get('songid', ''),
self.usermetadata.get('albumid', ''),
self.usermetadata.get('album_tracknr', '1'),
- self.usermetadata.get('album_tracknr', '1'),
imagedata['image'],
self.usermetadata["cover"],
imagedata['mime_type'],
'',
'',
self.extension,
- '',
'user'
)
\ No newline at end of file
diff --git a/metatube/metadata/readMetadata.py b/metatube/metadata/readMetadata.py
index 24c96a83..85afcc23 100644
--- a/metatube/metadata/readMetadata.py
+++ b/metatube/metadata/readMetadata.py
@@ -87,14 +87,12 @@ def readAudioMetadata(filename, songid, cover_path) -> MetadataObject:
songid,
albumid,
audio.get('tracknumber', [''])[0], # type: ignore
- 1,
coverdata['image'],
cover_path,
coverdata['mime_type'],
audio.get('isrc', [''])[0], # type: ignore
audio.get('lyrics', [''])[0], # type: ignore
extension,
- "",
source
)
@@ -118,13 +116,11 @@ def readVideoMetadata(filename, songid, cover_path) -> MetadataObject:
songid,
'',
1,
- 1,
coverdata['image'],
cover_path,
coverdata['mime_type'],
"Unknown",
"",
extension,
- "",
''
)
\ No newline at end of file
diff --git a/metatube/overview/routes.py b/metatube/overview/routes.py
index bc4d8420..72adb7b2 100644
--- a/metatube/overview/routes.py
+++ b/metatube/overview/routes.py
@@ -7,6 +7,9 @@
from metatube.youtube.youtubeUtils import utils as ytutils
from metatube.youtube.downloadOptions import downloadOptions
from metatube.metadata.processMetadata import processMetadata
+from metatube.metadata.readMetadata import readMetadata
+from metatube.metadata.mergeMetadata import mergeMetadata
+from metatube.metadata.metadataObject import MetadataObject
from metatube.deezer import Deezer
from metatube.spotify import spotify_metadata as Spotify
from metatube.genius import Genius
@@ -22,7 +25,6 @@
import metatube.musicbrainz as musicbrainz
import json
import os
-import requests
import random
import string
@@ -410,14 +412,14 @@ def editmetadata(id):
return False
extension = item.filepath.split('.')[len(item.filepath.split('.')) - 1].upper()
if extension in ['MP3', 'OPUS', 'FLAC', 'OGG']:
- metadata = MetaData.readaudiometadata(item.filepath)
+ metadata = readMetadata.readAudioMetadata(item.filepath, item.songid, item.cover).metadataMapper()
elif extension in ["M4A", 'MP4']:
- metadata = MetaData.readVideoMetadata(item.filepath)
+ metadata = readMetadata.readVideoMetadata(item.filepath, item.songid, item.cover).metadataMapper()
else:
return False
- metadata["songid"] = item.songid
metadata["itemid"] = item.id
- metadata["cover"] = item.cover
+ metadata["filename"] = item.filepath
+ metadata.pop('cover', 'cover_mime_type')
metadata_sources = Config.get_metadata_sources()
metadataform = render_template('metadataform.html', metadata_sources=metadata_sources)
sockets.editmetadata({'metadata': metadata, 'metadataview': metadataform})
@@ -432,7 +434,6 @@ def editfile(id):
'name': item.name,
'album': item.album,
'date': item.date,
- 'length': item.length,
'songid': item.songid,
'youtube_id': item.youtube_id,
'itemid': item.id
@@ -444,73 +445,53 @@ def editfile(id):
downloadform = render_template('downloadform.html', templates=templates, segments=segments, default=defaulttemplate)
sockets.editfile({'data': itemdata, 'downloadview': downloadform})
-# @socketio.on('editfilerequest')
-# def editfilerequest(filepath, id):
-# item = Database.fetchitem(id)
-# if item is not None:
-# extension = item.filepath.split('.')[len(item.filepath.split('.')) - 1].upper()
-# new_extension = filepath.split('.')[len(item.filepath.split('.')) - 1].upper()
-# if item.cover != env.DEFAULT_COVER_PATH:
-# try:
-# response = requests.get(item.cover)
-# image = response.content
-# magic = Magic(mime=True)
-# mime_type = magic.from_buffer(image)
-# except Exception:
-# sockets.downloadprocesserror('Cover URL is invalid!')
-# return False
-# else:
-# file = open(item.cover, 'rb')
-# image = file.read()
-# mime_type = 'image/png'
-# if extension in ['MP3', 'OPUS', 'FLAC', 'OGG']:
-# metadata_item = MetaData.readaudiometadata(item.filepath)
-
-# elif extension in ['MP4', 'M4A']:
-# metadata_item = MetaData.readVideoMetadata(item.filepath)
-# metadata_item["barcode"] = ""
-# metadata_item["language"] = ""
-
-# metadata_item["songid"] = item.songid
-# metadata_item["cover_path"] = item.cover
-# metadata_item["cover_mime_type"] = mime_type
-# metadata_item["image"] = image
-# metadata_item["itemid"] = item.id
-# metadata_item["goal"] = 'edit'
-# metadata_item["extension"] = new_extension
-# metadata_item["filename"] = filepath
-
-# if new_extension in ['MP3', 'OPUS', 'FLAC', 'OGG']:
-# MetaData.mergeaudiodata(metadata_item)
-# elif new_extension in ['MP4', 'M4A']:
-# MetaData.mergevideodata(metadata_item)
-# head, tail = os.path.split(filepath)
-# move(filepath, os.path.join(head, tail[4:len(tail)]))
-# try:
-# os.unlink(item.filepath)
-# except Exception:
-# pass
-# logger.info('Edited file %s', tail)
-# else:
-# logger.info('File not in database')
-
@socketio.on('editmetadatarequest')
-def editmetadatarequest(metadata_user, filepath, id):
+def editmetadatarequest(usermetadata, filepath, id):
extension = filepath.split('.')[len(filepath.split('.')) - 1].upper()
- data = MetaData.onlyuserdata(filepath, metadata_user)
- if data is not False:
- data["goal"] = 'edit'
- data["itemid"] = id
- data["extension"] = extension
- data["source"] = metadata_user["source"]
- if extension in ['MP3', 'OPUS', 'FLAC', 'OGG']:
- MetaData.mergeaudiodata(data)
- elif extension in ['MP4', 'M4A']:
- MetaData.mergevideodata(data)
- elif extension in ['WAV']:
- MetaData.mergeid3data(data)
- else:
- return False
+ item = Database.fetchitem(id)
+ if item is None:
+ return
+ cover = readMetadata.getImage(usermetadata['cover'])
+ metadata = MetadataObject(
+ usermetadata['title'],
+ usermetadata['artists'],
+ usermetadata['album'],
+ usermetadata['genres'],
+ usermetadata['language'],
+ usermetadata['release_date'],
+ usermetadata['songid'],
+ usermetadata['albumid'],
+ usermetadata['tracknr'],
+ cover['image'],
+ usermetadata['cover'],
+ cover['mime_type'],
+ usermetadata['isrc'],
+ usermetadata['lyrics'],
+ extension,
+ usermetadata['source']
+ )
+ mergedata = mergeMetadata(filepath, 'edit', metadata, item.youtube_id)
+ # data["goal"] = 'edit'
+ # data["itemid"] = id
+ # data["extension"] = extension
+ # data["source"] = usermetadata["source"]
+ if extension in ['MP3', 'OPUS', 'FLAC', 'OGG']:
+ data = mergedata.mergeaudiodata()
+ elif extension in ['MP4', 'M4A']:
+ data = mergedata.mergevideodata()
+ elif extension in ['WAV']:
+ data = mergedata.mergeid3data()
+ item.update({
+ 'filepath': item.filepath,
+ 'name': usermetadata['title'],
+ 'artist': usermetadata['artists'],
+ 'album': usermetadata['album'],
+ 'date': parser.parse(usermetadata["release_date"]),
+ 'image': usermetadata['cover'],
+ 'songid': usermetadata['songid'],
+ 'youtube_id': item.youtube_id
+ })
+ sockets.changed_metadata(data) # type: ignore
@bp.context_processor
def utility_processor():
diff --git a/metatube/sockets.py b/metatube/sockets.py
index fc7956b6..333fcaf8 100644
--- a/metatube/sockets.py
+++ b/metatube/sockets.py
@@ -88,5 +88,8 @@ def finished_metadata(data):
def changed_metadata(data):
socketio.emit('changed_metadata', data)
+def changed_filedata(data):
+ socketio.emit('changed_filedata', data)
+
def inserted_song(data):
socketio.emit('inserted_song', data)
\ No newline at end of file
diff --git a/metatube/spotify.py b/metatube/spotify.py
index cbc68d47..cd426031 100644
--- a/metatube/spotify.py
+++ b/metatube/spotify.py
@@ -12,7 +12,7 @@ def __init__(self, id, secret):
raise InvalidSpotifyCredentials('Spotify authentication has failed. Error: %s', str(e))
def search(self, data):
- searchresults = self.spotify.search(f"track:{data['title']}", data["max"])
+ searchresults = self.spotify.search(f"{data['title']}", data["max"])
searchresults["query"] = data["title"] # type: ignore
sockets.spotifysearch(searchresults)
logger.info('Searched Spotify for track \'%s\' ', data["title"])
diff --git a/metatube/static/JS/overview.js b/metatube/static/JS/overview.js
index 91bc2be1..3421a228 100644
--- a/metatube/static/JS/overview.js
+++ b/metatube/static/JS/overview.js
@@ -692,7 +692,7 @@ $(document).ready(function() {
deletebtn.classList.add('deleteitembtn');
if(youtube_id != null) {
- youtubebtn.href = youtube_id;
+ youtubebtn.href = 'https://youtu.be/' + youtube_id;
$("tr#" + rowid).find('.dropdown-menu').append(youtubebtn, deletebtn);
}
$("tr#" + rowid).find('.dropdown-menu').children(':first-child').before(editfilebtn, editmetadatabtn, downloaditembtn, playitembtn, moveitembtn);
@@ -732,16 +732,14 @@ $(document).ready(function() {
});
let artists = $("#md_artists").val().split(';');
- let albumartists = $("#md_album_artists").val().split(';');
return {
'songid': songid,
'albumid': albumid,
'title': $("#md_title").val(),
'artists': JSON.stringify(artists),
'album': $("#md_album").val(),
- 'album_artists': JSON.stringify(albumartists),
'album_tracknr': $("#md_album_tracknr").val(),
- 'album_releasedate': $("#md_album_releasedate").val(),
+ 'album_releasedate': $("#md_release_date").val(),
'cover': $("#md_cover").val(),
'people': JSON.stringify(people),
'songid': songid,
@@ -750,12 +748,35 @@ $(document).ready(function() {
};
}
+ function updateRow(data) {
+ let tr = $("tr#"+data.itemid);
+ tr.find('img').attr('src', data.image);
+ tr.find('img').siblings('span').text(data.name);
+ tr.find('.td_artist').text(data.artist);
+ tr.find('.td_album').text(data.album);
+ tr.find('.td_date').text(data.date);
+ tr.find('.td_filepath').text(data.filepath.split('.')[data.filepath.split('.').length - 1]);
+ }
+
function getPhases() {
return $("#segments_check").is(':checked') ? 4 : 5;
}
function getProgress() {
- return $("#edititemmodal").css('display').toLowerCase() != 'none' ? $("#progressedit") : $("#progress");
+ if($("#editmetadatamodal").css('display').toLowerCase() != 'none' || $("#editfilemodal").css('display').toLowerCase() != 'none') {
+ if($("#editmetadatamodal").css('display').toLowerCase() != 'none') {
+ return $('#editmetadatamodal').find('.progressedit');
+ }
+ return $('#editfilemodal').find('.progressedit');
+ }
+ return $("#progress");
+ }
+
+ function isChanging() {
+ if($("#editmetadatamodal").css('display').toLowerCase() != 'none' || $("#editfilemodal").css('display').toLowerCase() != 'none') {
+ return true;
+ }
+ return false;
}
function setProgress(percentage) {
@@ -1016,13 +1037,10 @@ $(document).ready(function() {
$(document).on('click', '#editmetadatabtnmodal', function() {
if($("#metadatasection").find('input[required]').val() == '') {
$("#metadatalog").text('Fill all required fields!');
- } else if($("#outputname").val().startsWith('tmp_')) {
- $("#downloadmodal").animate({ scrollTop: 0 }, 'fast');
- $("#metadatalog").text('Your output name can not begin with tmp_!');
} else {
let people = {};
let filepath = $("#item_filepath").val();
- let id = $("#edititemmodal").attr('itemid');
+ let id = $("#editmetadatamodal").attr('itemid');
let songid = $("#spotify_songid").length > 0 ? $("#spotify_songid").val() : $("#mbp_songid").val();
let albumid = $("#spotify_albumid").length > 0 ? $("#spotify_albumid").val() : $("#mbp_albumid").val();
let source = $("#spotify_songid").length > 0 ? 'Spotify' : 'Musicbrainz';
@@ -1044,34 +1062,50 @@ $(document).ready(function() {
'songid': songid,
'albumid': albumid,
'title': $("#md_title").val(),
+ 'genres': $("#md_genres").val(),
'artists': $("#md_artists").val(),
'album': $("#md_album").val(),
- 'album_artists': $("#md_album_artists").val(),
- 'album_tracknr': $("#md_album_tracknr").val(),
- 'album_releasedate': $("#md_album_releasedate").val(),
+ 'tracknr': $("#md_album_tracknr").val(),
+ 'release_date': $("#md_release_date").val(),
+ 'language': $("#md_language").val(),
'cover': $("#md_cover").val(),
'people': JSON.stringify(people),
+ 'isrc': $("#md_isrc").val(),
+ 'lyrics': '',
'source': source
};
socket.emit('editmetadatarequest', metadata, filepath, id);
- $("#progressedit").attr({
+ $("#editmetadatamodal").find(".progressedit").attr({
'aria-valuenow': '66',
'aria-valuemin': '0',
'style': 'width: 66%'
});
- $("#progresstextedit").text('Skipping download and processing, adding metadata...');
- $("#downloadsection, #metadatasection, #editmetadatabtnmodal").addClass('d-none');
- $("#progressection").removeClass('d-none');
+ $("#editmetadatamodal").find(".progresstextedit").text('Skipping download and processing, adding metadata...');
+ $("#metadatasection, #editmetadatabtnmodal").addClass('d-none');
+ $("#editmetadatamodal").find(".progressection").removeClass('d-none');
}
});
+ $(document).on('click', '#searchformetadata', function() {
+ $("#metadatasection, #progressection, #editmetadatabtnmodal, #editfilebtnmodal").addClass('d-none');
+ $("#searchmetadatasection").removeClass('d-none');
+ })
+
+ // $(document).on('click', '#searchnewmetadatabtn', function() {
+ // let args = {
+ // 'title': $("#newmetadataquery").val(),
+ // 'artist': "",
+ // 'type': 'webui'
+ // }
+ // socket.emit('searchmetadata', args);
+ // })
+
$(document).on('click', '#editfilebtnmodal', function() {
$("#downloadsection").find('h5').after('
');
if($(".timestamp_input").val() == '' && !$("#segments_check").is(':checked')) {
$("#downloadmodal").animate({ scrollTop: 0 }, 'fast');
$("#editfilelog").text('Enter all segment fields or disable the segments');
} else {
- let url = $("#edititemmodal").attr('youtube_id');
let ext = $("#extension").val();
let output_folder = $("#output_folder").val();
let type = $("#type").val();
@@ -1111,13 +1145,14 @@ $(document).ready(function() {
'width': width,
'height': height,
'goal': 'edit',
- 'itemid': document.getElementById('edititemmodal').dataset.itemid,
- 'youtube_id': document.getElementById('edititemmodal').dataset.youtube_id,
+ 'itemid': document.getElementById('editfilemodal').dataset.itemid,
+ 'youtube_id': document.getElementById('editfilemodal').dataset.youtube_id,
}
socket.emit('ytdl_download', filedata, function(ack) {
if(ack == "OK") {
+ progress_text = $("#editfilemodal").find(".progresstextedit");
$("#metadatasection, #downloadsection, #editfilebtnmodal").addClass('d-none');
- $("#progressection").removeClass('d-none');
+ $("#editfilemodal").find(".progressection").removeClass('d-none');
}
});
}
@@ -1460,11 +1495,12 @@ $(document).ready(function() {
'userMetadata': getMetadata(),
}
socket.emit('ytdl_download', filedata, function(ack) {
- progress_text = $("#edititemmodal").css('display').toLowerCase() != 'none' ? $("#progresstextedit") : $("#progresstext");
+ progress_text = $("#progresstext");
if(ack == "OK") {
$("#editmetadata, #downloadbtn, #searchmetadataview, #404p, #defaultview, #resetviewbtn, #geniusbtn, #audiocol, #savemetadata, #metadataview, #geniuscol").addClass('d-none');
$("#progressview").removeClass('d-none');
$("#searchlog").empty();
+ progress_text.val('Initializing download...');
}
});
}
@@ -1495,12 +1531,12 @@ $(document).ready(function() {
$("#downloadmodal").modal('toggle');
});
- $("#downloadmodal, #removeitemmodal, #edititemmodal").on('hidden.bs.modal', function() {
+ $("#downloadmodal, #removeitemmodal, #editfilemodal, #editmetadatamodal").on('hidden.bs.modal', function() {
$(this).removeClass(['d-flex', 'justify-content-center']);
});
$("#downloadfilebtn").on('click', function() {
- socket.emit('downloaditem', $(this).attr('filepath'));
+ socket.emit('downloaditem', $(this).attr('itemid'));
});
$("#resetviewbtn").on('click', function() {
@@ -1540,13 +1576,12 @@ $(document).ready(function() {
// $("#defaultview").children('#audiocol').remove();
spinner('Loading metadata...', $("#defaultview"));
});
-
+
socket.on('downloadprogress', function(msg) {
$("#editmetadata, #nextbtn, #defaultview, #ytcol").addClass('d-none');
$("#progressview").removeClass('d-none');
$("#searchlog").empty();
- // var progress_text = $("#edititemmodal").css('display').toLowerCase() != 'none' ? $("#progresstextedit") : $("#progresstext");
-
+ console.log(msg);
if(msg.total_bytes != 'Unknown') {
if((msg.downloaded_bytes / msg.total_byes) == 1) {
progress_text.text("Extracting audio...");
@@ -1562,17 +1597,19 @@ $(document).ready(function() {
});
socket.on('finished_download', function() {
+ console.log('finished download');
let percentage = 100 / getPhases();
setProgress(percentage);
progress_text.text('Extracting audio...');
});
- socket.on('postprocessing', function(msg) {
- if(msg.postprocessor == 'ModifyChapters') {
+ socket.on('postprocessing', function(postprocessor) {
+ console.log(postprocessor);
+ if(postprocessor == 'ModifyChapters') {
let percentage = (100 / getPhases()) * 2;
setProgress(percentage);
progress_text.text('Cutting segments from the video... ');
- } else if(msg.postprocessor == 'MoveFiles') {
+ } else if(postprocessor == 'MoveFiles') {
let percentage = (100 / getPhases()) * 3;
setProgress(percentage);
progress_text.text('Moving the files to its destination... ');
@@ -1580,15 +1617,14 @@ $(document).ready(function() {
});
socket.on('finished_postprocessor', function(postprocessor) {
+ console.log(postprocessor);
if(postprocessor == 'MoveFiles') {
let percentage = (100 / getPhases()) * (getPhases() - 1);
setProgress(percentage);
progress_text.text('Adding metadata...');
var filepath = msg.filepath;
- if($("#edititemmodal").css('display').toLowerCase() == 'none') {
- socket.emit('mergedata', getMetadata(), filepath);
- } else {
- let itemid = $("#edititemmodal").attr('itemid');
+ if(isChanging() === true) {
+ let itemid = $("#editfilemodal").attr('itemid');
socket.emit('editfilerequest', filepath, itemid);
}
}
@@ -1597,28 +1633,24 @@ $(document).ready(function() {
socket.on('finished_metadata', function(data) {
setProgress("100");
progress_text.text('Finished adding metadata!');
- try {
- $("#downloadfilebtn").removeClass('d-none');
- $("#downloadfilebtn").attr('filepath', data["filepath"]);
- } catch (error) {
- console.error(error);
- }
});
+ socket.on('changed_file', function(data) {
+ updateRow(data);
+ $("#overviewlog").text("File has been changed!");
+ $("#editfilemodal").modal('hide');
+ })
+
socket.on('changed_metadata', function(data) {
- let tr = $("tr#"+data.itemid);
- tr.find('img').attr('src', data.image);
- tr.find('img').siblings('span').text(data.name);
- tr.find('.td_artist').text(data.artist);
- tr.find('.td_album').text(data.album);
- tr.find('.td_date').text(data.date);
- tr.find('.td_filepath').text(data.filepath.split('.')[data.filepath.split('.').length - 1]);
+ updateRow(data);
$("#overviewlog").text("Item metadata has been changed!");
- $("#edititemmodal").modal('hide');
+ $("#editmetadatamodal").modal('hide');
});
socket.on('inserted_song', function(data) {
$("#overviewlog").empty();
+ $("#downloadfilebtn").removeClass('d-none');
+ $("#downloadfilebtn").attr('itemid', data["id"]);
additem(data)
})
@@ -1636,7 +1668,7 @@ $(document).ready(function() {
getProgress().attr('aria-valuenow', 100);
getProgress().html('ERROR ');
getProgress().css('width', '100%');
- if($("#edititemmodal").css('display').toLowerCase() != 'block') {
+ if($("#editfilemodal").css('display').toLowerCase() != 'block') {
$("#resetviewbtn").removeClass('d-none');
}
})
@@ -1778,7 +1810,7 @@ $(document).ready(function() {
$("#md_title").val(title);
$("#md_artists").val(artists);
$("#md_album").val(album);
- $("#md_album_releasedate").val(album_releasedate);
+ $("#md_release_date").val(album_releasedate);
$("#mbp_albumid").val(album_id);
if("artist-relation-list" in mbp["release"] && mbp["release"]["artist-relation-list"].length > 0) {
@@ -1816,8 +1848,7 @@ $(document).ready(function() {
});
artists = artists.trim().slice(0, artists.trim().length -1);
$("#md_cover").val(release_cover);
- $("#md_album_releasedate").val(release_date);
- $("#md_album_artists").val(artists);
+ $("#md_release_date").val(release_date);
$("#md_album_tracknr").val(tracknr);
});
@@ -1841,9 +1872,8 @@ $(document).ready(function() {
$("#md_title").val(data["name"]);
$("#md_artists").val(artists.slice(0, artists.length - 2));
$("#md_album").val(data["album"]["name"]);
- $("#md_album_artists").val(albumartists.slice(0, albumartists.length - 2))
$("#md_album_tracknr").val(data["track_number"]);
- $("#md_album_releasedate").val(data["album"]["release_date"]);
+ $("#md_release_date").val(data["album"]["release_date"]);
$("#spotify_albumid").val(data["album"]["id"]);
$("#md_cover").val(cover);
});
@@ -1857,9 +1887,8 @@ $(document).ready(function() {
$("#md_title").val(data["title"]);
$("#md_artists").val(contributors);
$("#md_album").val(data["album"]["title"]);
- $("#md_album_artists").val(data["artist"]["name"])
$("#md_album_tracknr").val(data["track_position"]);
- $("#md_album_releasedate").val(data["release_date"]);
+ $("#md_release_date").val(data["release_date"]);
$("#spotify_albumid").val(data["album"]["id"]);
$("#md_cover").val(data["album"]["cover_medium"]);
});
@@ -1875,7 +1904,7 @@ $(document).ready(function() {
$("#md_artists").val(artists.slice(0, artists.length - 2));
$("#md_album").val(songdata["album"]["name"]);
$("#md_cover").val(songdata["song_art_image_thumbnail_url"]);
- $("#md_album_releasedate").val(songdata["release_date"]);
+ $("#md_release_date").val(songdata["release_date"]);
$("#genius_albumid").val(songdata["album"]["id"]);
});
@@ -1893,7 +1922,6 @@ $(document).ready(function() {
$("#md_album_tracknr").val(i + 1);
}
}
- $("#md_album_artists").val(albumartists);
});
socket.on('searchvideo', (data) => {
@@ -1974,21 +2002,20 @@ $(document).ready(function() {
socket.on('edit_metadata', (data) => {
$("#downloadsection, #metadatasection, #metadataview").empty();
$("#metadatasection").append(data.metadataview);
- $("#metadatasection").find('#mbp_songid').val(data.metadata.musicbrainz_id);
- $("#metadatasection").find('#mbp_albumid').val(data.metadata.mbp_releasegroupid);
+ $("#metadatasection").find('#mbp_songid').val(data.metadata.songid);
+ $("#metadatasection").find('#mbp_albumid').val(data.metadata.albumid);
$("#metadatasection").find('#md_title').val(data.metadata.title);
$("#metadatasection").find('#md_artists').val(data.metadata.artists);
$("#metadatasection").find('#md_album').val(data.metadata.album);
- $("#metadatasection").find('#md_album_artists').val(data.metadata.artists);
$("#metadatasection").find('#md_album_tracknr').val(data.metadata.tracknr);
- $("#metadatasection").find('#md_album_releasedate').val(data.metadata.date);
- $("#metadatasection").find('#md_cover').val(data.metadata.cover);
+ $("#metadatasection").find('#md_release_date').val(data.metadata.date);
+ $("#metadatasection").find('#md_cover').val(data.metadata.cover_path);
$("#metadatasection").prepend('');
- $("#edititemmodal").attr('itemid', data.metadata.itemid)
- $("#editfilebtnmodal").attr('id','editmetadatabtnmodal');
+ $("#editmetadatamodal").attr('itemid', data.metadata.itemid)
+ document.getElementById('editmetadatamodaltitle').innerText = 'Changing metadata of ' + data.metadata.title;
$("#metadatasection, #editmetadatabtnmodal").removeClass('d-none');
- $("#progressection").addClass('d-none');
- $("#progressedit").attr({
+ $("#editmetadatamodal").find(".progressection").addClass('d-none');
+ $(".progressedit").attr({
'aria-valuenow': '0',
'aria-valuemin': '0',
'style': ''
@@ -1997,8 +2024,8 @@ $(document).ready(function() {
$("#spotify_songid").val(data.metadata.songid);
}
- $("#edititemmodal").addClass(['d-flex', 'justify-content-center']);
- $("#edititemmodal").modal('show');
+ $("#editmetadatamodal").addClass(['d-flex', 'justify-content-center']);
+ $("#editmetadatamodal").modal('show');
});
socket.on('metadatalog', (msg) => {
@@ -2008,20 +2035,19 @@ $(document).ready(function() {
socket.on('edit_file', (data) => {
$("#downloadsection, #ytcol, #metadatasection").empty();
$("#downloadsection").append(data.downloadview);
- $("#progressection").addClass('d-none');
- $("#progressedit").attr({
+ $("#editfilemodal").find(".progressection").addClass('d-none');
+ $(".progressedit").attr({
'aria-valuenow': '0',
'aria-valuemin': '0',
'style': ''
});
- $("#editmetadatabtnmodal").attr('id', 'editfilebtnmodal');
$("#downloadsection, #editfilebtnmodal").removeClass('d-none');
$("#downloadsection").find('hr').remove();
- $("#edititemmodal").attr({'data-itemid': data.data.itemid, 'data-youtube_id': data.data.youtube_id});
+ $("#editfilemodal").attr({'data-itemid': data.data.itemid, 'data-youtube_id': data.data.youtube_id});
$("hr").addClass('d-none');
- $("#edititemmodal").addClass(['d-flex', 'justify-content-center']);
- $("#edititemmodal").modal('show');
+ $("#editfilemodal").addClass(['d-flex', 'justify-content-center']);
+ $("#editfilemodal").modal('show');
});
socket.on('youtubesearch', (data) => {
diff --git a/metatube/templates/metadataform.html b/metatube/templates/metadataform.html
index edb4fbac..9bdc8efd 100644
--- a/metatube/templates/metadataform.html
+++ b/metatube/templates/metadataform.html
@@ -7,9 +7,9 @@
{% if 'musicbrainz' in metadata_sources %}