diff --git a/picard/tagger.py b/picard/tagger.py index 3eb11cca94..fdc7c2d103 100644 --- a/picard/tagger.py +++ b/picard/tagger.py @@ -55,6 +55,7 @@ import shutil import signal import sys +from urllib.parse import urlparse from PyQt5 import ( QtCore, @@ -174,6 +175,30 @@ def plugin_dirs(): yield USER_PLUGIN_DIR +class ParseItemsToLoad: + + def __init__(self, items): + self.files = set() + self.mbids = set() + self.urls = set() + + for item in items: + parsed = urlparse(item) + if not parsed.scheme: + self.files.add(item) + elif parsed.scheme == "file": + # remove file:// prefix safely + self.files.add(item[7:]) + elif parsed.scheme == "mbid": + self.mbids.add(parsed.netloc + parsed.path) + elif parsed.scheme in {"http", "https"}: + # .path returns / before actual link + self.urls.add(parsed.path[1:]) + + def __bool__(self): + return bool(self.files or self.mbids or self.urls) + + class Tagger(QtWidgets.QApplication): tagger_stats_changed = QtCore.pyqtSignal() @@ -196,7 +221,7 @@ def __init__(self, picard_args, localedir, autoupdate, pipe_handler=None): config = get_config() theme.setup(self) - self._cmdline_files = picard_args.FILE + self._cmdline_files = picard_args.FILE_OR_URL self.autoupdate_enabled = autoupdate self._no_restore = picard_args.no_restore self._no_plugins = picard_args.no_plugins @@ -322,8 +347,21 @@ def pipe_server(self): while self.pipe_handler.pipe_running: messages = [x for x in self.pipe_handler.read_from_pipe() if x not in IGNORED] if messages: - self.add_paths(messages) - self.bring_tagger_front() + self.load_to_picard(messages) + + def load_to_picard(self, items): + parsed_items = ParseItemsToLoad(items) + + if parsed_items.files: + self.add_paths(parsed_items.files) + + if parsed_items.urls or parsed_items.mbids: + file_lookup = self.get_file_lookup() + for item in parsed_items.mbids | parsed_items.urls: + thread.to_main(file_lookup.mbid_lookup, item, None, None, False) + + if parsed_items: + self.bring_tagger_front() def enable_menu_icons(self, enabled): self.setAttribute(QtCore.Qt.ApplicationAttribute.AA_DontShowIconsInMenus, not enabled) @@ -431,8 +469,7 @@ def exit(self): def _run_init(self): if self._cmdline_files: - files = [decode_filename(f) for f in self._cmdline_files] - self.add_paths(files) + self.load_to_picard([decode_filename(f) for f in self._cmdline_files]) del self._cmdline_files def run(self): @@ -1071,7 +1108,8 @@ def process_picard_args(): help="display version information and exit") parser.add_argument("-V", "--long-version", action='store_true', help="display long version information and exit") - parser.add_argument('FILE', nargs='*') + parser.add_argument('FILE_OR_URL', nargs='*', + help="the file(s), URL(s) and MBID(s) to load") return parser.parse_known_args()[0] @@ -1113,8 +1151,14 @@ def main(localedir=None, autoupdate=True): } if not should_start: + to_be_added = [] + for x in picard_args.FILE_OR_URL: + if not urlparse(x).netloc: + x = os.path.abspath(x) + to_be_added.append(x) + try: - pipe_handler = pipe.Pipe(app_name=PICARD_APP_NAME, app_version=PICARD_FANCY_VERSION_STR, args=[os.path.abspath(x) for x in picard_args.FILE]) + pipe_handler = pipe.Pipe(app_name=PICARD_APP_NAME, app_version=PICARD_FANCY_VERSION_STR, args=to_be_added) should_start = pipe_handler.is_pipe_owner except pipe.PipeErrorNoPermission as err: log.error(err) diff --git a/test/test_tagger_message_parsing.py b/test/test_tagger_message_parsing.py new file mode 100644 index 0000000000..f01cc40e4e --- /dev/null +++ b/test/test_tagger_message_parsing.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# +# Picard, the next-generation MusicBrainz tagger +# +# Copyright (C) 2022 skelly37 +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +from test.picardtestcase import PicardTestCase + +from picard.tagger import ParseItemsToLoad + + +class TestMessageParsing(PicardTestCase): + def test(self): + test_cases = { + "test_case.mp3", + "file:///home/picard/music/test.flac", + "mbid://recording/7cd3782d-86dc-4dd1-8d9b-e37f9cbe6b94", + "https://musicbrainz.org/recording/7cd3782d-86dc-4dd1-8d9b-e37f9cbe6b94", + "http://musicbrainz.org/recording/7cd3782d-86dc-4dd1-8d9b-e37f9cbe6b94", + } + + result = ParseItemsToLoad(test_cases) + self.assertSetEqual(result.files, {"test_case.mp3", "/home/picard/music/test.flac"}, "Files test") + self.assertSetEqual(result.mbids, {"recording/7cd3782d-86dc-4dd1-8d9b-e37f9cbe6b94"}, "MBIDs test") + self.assertSetEqual(result.urls, {"recording/7cd3782d-86dc-4dd1-8d9b-e37f9cbe6b94", + "recording/7cd3782d-86dc-4dd1-8d9b-e37f9cbe6b94"}, "URLs test") + + def test_bool_files_true(self): + test_cases = { + "test_case.mp3", + } + self.assertTrue(ParseItemsToLoad(test_cases)) + + def test_bool_mbids_true(self): + test_cases = { + "mbid://recording/7cd3782d-86dc-4dd1-8d9b-e37f9cbe6b94", + } + self.assertTrue(ParseItemsToLoad(test_cases)) + + def test_bool_urls_true(self): + test_cases = { + "https://musicbrainz.org/recording/7cd3782d-86dc-4dd1-8d9b-e37f9cbe6b94", + } + self.assertTrue(ParseItemsToLoad(test_cases)) + + def test_bool_invalid_false(self): + test_cases = { + "mbd://recording/7cd3782d-86dc-4dd1-8d9b-e37f9cbe6b94", + } + self.assertFalse(ParseItemsToLoad(test_cases)) + + def test_bool_empty_false(self): + test_cases = {} + self.assertFalse(ParseItemsToLoad(test_cases)) diff --git a/test/test_util_pipe.py b/test/test_util_pipe.py index be9389b36c..67f1051857 100644 --- a/test/test_util_pipe.py +++ b/test/test_util_pipe.py @@ -61,6 +61,11 @@ def test_pipe_protocol(self): "my_music_file.mp3", TestPipe.NAME, TestPipe.VERSION, "last-case", + "https://test-ca.se/index.html", + "file:///data/test.py", + "www.wikipedia.mp3", + "mbid://recording/7cd3782d-86dc-4dd1-8d9b-e37f9cbe6b94", + "https://musicbrainz.org/recording/7cd3782d-86dc-4dd1-8d9b-e37f9cbe6b94", ) pipe_listener_handler = pipe.Pipe(self.NAME, self.VERSION)