From 14cd430bac66a9361d390b380bc27509425e3a73 Mon Sep 17 00:00:00 2001 From: Andrew Rogl Date: Sat, 2 Jul 2022 15:18:36 +1000 Subject: [PATCH 01/68] Preserve mtimes from archives #4392 --- beets/importer.py | 13 +++++++++++++ docs/changelog.rst | 2 ++ 2 files changed, 15 insertions(+) diff --git a/beets/importer.py b/beets/importer.py index 561cedd2c..6e0d62367 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -1090,6 +1090,19 @@ class ArchiveImportTask(SentinelImportTask): archive = handler_class(util.py3_path(self.toppath), mode='r') try: archive.extractall(extract_to) + + # From here: + # https://stackoverflow.com/questions/9813243/extract-files-from-zip-file-and-retain-mod-date + # fixing #4392 + + for f in archive.infolist(): + # path to this extracted f-item + fullpath = os.path.join(extract_to, f.filename) + # still need to adjust the dt o/w item will have the current dt + date_time = time.mktime(f.date_time + (0, 0, -1)) + # update date_time + os.utime(fullpath, (date_time, date_time)) + finally: archive.close() self.extracted = True diff --git a/docs/changelog.rst b/docs/changelog.rst index 3c61b3bf8..7a7fe15e5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -32,6 +32,8 @@ New features: * :ref:`import-options`: Add support for re-running the importer on paths in log files that were created with the ``-l`` (or ``--logfile``) argument. :bug:`4379` :bug:`4387` +* Preserve mtimes from archives + :bug:`4392` Bug fixes: From c2c617594fbcbee6583320cf34263fa76ea3d5ce Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Mon, 5 Sep 2022 09:03:02 +0200 Subject: [PATCH 02/68] Save Discogs Release ID via MusicBrainz - Enable fetching 'url-rels' from the MusicBrainz release endpoint. - A MusicBrainz release might contain a link to a Discogs release which we save to the discogs_albumid attribute of the info object. - For extraction of the release ID from the Discogs URL, we use the method provided in the id_extractors util module. --- beets/autotag/mb.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index f380cd033..c39f91c2d 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -29,6 +29,7 @@ from beets import util from beets import config from collections import Counter from urllib.parse import urljoin +from beets.util.id_extractors import extract_discogs_id_regex VARIOUS_ARTISTS_ID = '89ad4ac3-39f7-470e-963a-56509c546377' @@ -70,7 +71,7 @@ log = logging.getLogger('beets') RELEASE_INCLUDES = ['artists', 'media', 'recordings', 'release-groups', 'labels', 'artist-credits', 'aliases', 'recording-level-rels', 'work-rels', - 'work-level-rels', 'artist-rels', 'isrcs'] + 'work-level-rels', 'artist-rels', 'isrcs', 'url-rels'] BROWSE_INCLUDES = ['artist-credits', 'work-rels', 'artist-rels', 'recording-rels', 'release-rels'] if "work-level-rels" in musicbrainzngs.VALID_BROWSE_INCLUDES['recording']: @@ -511,6 +512,15 @@ def album_info(release: Dict) -> beets.autotag.hooks.AlbumInfo: in sorted(genres.items(), key=lambda g: -g[1]) ) + # We might find URLs to external sources (Discogs, Spotify, ...) + if release.get('url-relation-list'): + discogs_url = None + for url in release['url-relation-list']: + if url['type'] == 'discogs': + log.debug('Found link to Discogs release via MusicBrainz') + discogs_url = url['target'] + info.discogs_albumid = extract_release_id_regex(discogs_url) + extra_albumdatas = plugins.send('mb_album_extract', data=release) for extra_albumdata in extra_albumdatas: info.update(extra_albumdata) From 36daf93828ec4f5d8345fa03e76ebe307bd162b8 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Sun, 12 Mar 2023 16:28:29 -0400 Subject: [PATCH 03/68] Add instructions to install directly from Github. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index b894daddc..5f46c8d0d 100644 --- a/README.rst +++ b/README.rst @@ -80,7 +80,7 @@ shockingly simple if you know a little Python. Install ------- -You can install beets by typing ``pip install beets``. +You can install beets by typing ``pip install beets`` or directly from github using ``python -m pip install git+https://github.com/beetbox/beets.git``. command. Beets has also been packaged in the `software repositories`_ of several distributions. Check out the `Getting Started`_ guide for more information. From 53911c74ddd457503af1b292245134f510f9c3b5 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Mon, 13 Mar 2023 11:33:45 -0400 Subject: [PATCH 04/68] Link to dev docs --- README.rst | 2 +- docs/faq.rst | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 5f46c8d0d..47eed37be 100644 --- a/README.rst +++ b/README.rst @@ -80,7 +80,7 @@ shockingly simple if you know a little Python. Install ------- -You can install beets by typing ``pip install beets`` or directly from github using ``python -m pip install git+https://github.com/beetbox/beets.git``. command. +You can install beets by typing ``pip install beets`` or directly from Github (see details [here](https://beets.readthedocs.io/en/stable/faq.html#run-the-latest-source-version-of-beets)). Beets has also been packaged in the `software repositories`_ of several distributions. Check out the `Getting Started`_ guide for more information. diff --git a/docs/faq.rst b/docs/faq.rst index 5e1c73666..b5a6cea0e 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -137,6 +137,8 @@ it's helpful to run on the "bleeding edge". To run the latest source: ``pip uninstall beets``. 2. Install from source. Choose one of these methods: + - Directly from github using + ``python -m pip install git+https://github.com/beetbox/beets.git`` command. Depending on your system, you may need to use ``pip3`` and ``python3`` instead of ``pip`` and ``python`` respectively. - Use ``pip`` to install the latest snapshot tarball. Type: ``pip install https://github.com/beetbox/beets/tarball/master`` - Grab the source using git. First, clone the repository: From 05cdecfb7cdb1e8d5d7387ab5a7f451b5919f8d0 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Tue, 14 Mar 2023 20:39:47 -0400 Subject: [PATCH 05/68] Add more examples to convert plugin --- docs/plugins/convert.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/plugins/convert.rst b/docs/plugins/convert.rst index 7adb94079..4aed7c90a 100644 --- a/docs/plugins/convert.rst +++ b/docs/plugins/convert.rst @@ -94,9 +94,9 @@ file. The available options are: output. Note that this does not guarantee that all converted files will have a lower bitrate---that depends on the encoder and its configuration. Default: none. -- **no_convert**: Does not transcode items matching provided query string - (see :doc:`/reference/query`). (i.e. ``format:AAC, format:WMA`` or - ``path::\.(m4a|wma)$``) +- **no_convert**: Does not transcode items matching the query string provided + (see :doc:`/reference/query`). For example, to not convert AAC or WMA formats, you can use ``format:AAC, format:WMA`` or + ``path::\.(m4a|wma)$``. If you only want to transcode WMA format, you can use the query term negation to not convert all the other formats except wma, e.g., ``^path::\.(wma)$``. - **never_convert_lossy_files**: Cross-conversions between lossy codecs---such as mp3, ogg vorbis, etc.---makes little sense as they will decrease quality even further. If set to ``yes``, lossy files are always copied. From 5f87371229465a79713a12f5917d4e2bf3fcae9b Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Tue, 14 Mar 2023 20:57:34 -0400 Subject: [PATCH 06/68] rephrase --- docs/plugins/convert.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/convert.rst b/docs/plugins/convert.rst index 4aed7c90a..622a8f2cd 100644 --- a/docs/plugins/convert.rst +++ b/docs/plugins/convert.rst @@ -96,7 +96,7 @@ file. The available options are: Default: none. - **no_convert**: Does not transcode items matching the query string provided (see :doc:`/reference/query`). For example, to not convert AAC or WMA formats, you can use ``format:AAC, format:WMA`` or - ``path::\.(m4a|wma)$``. If you only want to transcode WMA format, you can use the query term negation to not convert all the other formats except wma, e.g., ``^path::\.(wma)$``. + ``path::\.(m4a|wma)$``. If you only want to transcode WMA format, you can use a negative query, e.g., ``^path::\.(wma)$``, to not convert any other format except WMA. - **never_convert_lossy_files**: Cross-conversions between lossy codecs---such as mp3, ogg vorbis, etc.---makes little sense as they will decrease quality even further. If set to ``yes``, lossy files are always copied. From 6d0e5ba8cabaab8ef7f0b142162d69082e08b78a Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Fri, 17 Mar 2023 11:06:46 -0400 Subject: [PATCH 07/68] Change to GitHub --- docs/faq.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.rst b/docs/faq.rst index b5a6cea0e..e7f5cc600 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -137,7 +137,7 @@ it's helpful to run on the "bleeding edge". To run the latest source: ``pip uninstall beets``. 2. Install from source. Choose one of these methods: - - Directly from github using + - Directly from GitHub using ``python -m pip install git+https://github.com/beetbox/beets.git`` command. Depending on your system, you may need to use ``pip3`` and ``python3`` instead of ``pip`` and ``python`` respectively. - Use ``pip`` to install the latest snapshot tarball. Type: ``pip install https://github.com/beetbox/beets/tarball/master`` From 9f4b43c54076c9195974ccb79ac57ab313e0bf08 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Sat, 18 Mar 2023 10:56:16 -0400 Subject: [PATCH 08/68] Point to latest docs --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 47eed37be..62fbfc298 100644 --- a/README.rst +++ b/README.rst @@ -80,7 +80,7 @@ shockingly simple if you know a little Python. Install ------- -You can install beets by typing ``pip install beets`` or directly from Github (see details [here](https://beets.readthedocs.io/en/stable/faq.html#run-the-latest-source-version-of-beets)). +You can install beets by typing ``pip install beets`` or directly from Github (see details [here](https://beets.readthedocs.io/en/latest/faq.html#run-the-latest-source-version-of-beets)). Beets has also been packaged in the `software repositories`_ of several distributions. Check out the `Getting Started`_ guide for more information. From 5231037591959a2f088c0474afacd45e06737bf0 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Mon, 20 Mar 2023 17:32:45 -0400 Subject: [PATCH 09/68] Add JioSaavn as a tag source --- docs/plugins/index.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index ea13d2feb..c9e008023 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -458,6 +458,9 @@ Here are a few of the plugins written by the beets community: Lets you perform regex replacements on incoming metadata. +`beets-jiosaavn`_ + Adds JioSaavn.com as a tagger data source.. + `beets-mosaic`_ Generates a montage of a mosaic from cover art. @@ -522,6 +525,7 @@ Here are a few of the plugins written by the beets community: .. _beets-usertag: https://github.com/igordertigor/beets-usertag .. _beets-popularity: https://github.com/abba23/beets-popularity .. _beets-plexsync: https://github.com/arsaboo/beets-plexsync +.. _beets-jiosaavn: https://github.com/arsaboo/beets-jiosaavn .. _beets-ydl: https://github.com/vmassuchetto/beets-ydl .. _beet-summarize: https://github.com/steven-murray/beet-summarize .. _beets-mosaic: https://github.com/SusannaMaria/beets-mosaic From 4338ef3e3531039767d0c0db75151afb724c76d9 Mon Sep 17 00:00:00 2001 From: Andrew Rogl Date: Sat, 25 Mar 2023 13:36:27 +1000 Subject: [PATCH 10/68] Address comments from @sampsyo --- beets/importer.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/beets/importer.py b/beets/importer.py index 6e0d62367..5c65b57c1 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -1091,16 +1091,19 @@ class ArchiveImportTask(SentinelImportTask): try: archive.extractall(extract_to) - # From here: - # https://stackoverflow.com/questions/9813243/extract-files-from-zip-file-and-retain-mod-date - # fixing #4392 + # Adjust the files' mtimes to match the information from the archive. Inspired by: + # https://stackoverflow.com/q/9813243 for f in archive.infolist(): - # path to this extracted f-item - fullpath = os.path.join(extract_to, f.filename) # still need to adjust the dt o/w item will have the current dt - date_time = time.mktime(f.date_time + (0, 0, -1)) # update date_time + # Can you give a clarification why you add (0, 0, -1) to the date_time? + # Is the current a second off? + # (0, 0, -1) is added because time.mktime expects a 9-element tuple. + # The -1 indicates that the DST flag is unknown. + + date_time = time.mktime(f.date_time + (0, 0, -1)) + fullpath = os.path.join(extract_to, f.filename) os.utime(fullpath, (date_time, date_time)) finally: From 6f2c31926e7734c5b19cc28fe75bf678f5c1ac11 Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Sun, 25 Sep 2022 08:35:19 +0200 Subject: [PATCH 11/68] Fetch more external IDs via MusicBrainz Add retrieval and ID extraction of Bandcamp, Spotify, Deezer and Beatport URLs while retrieving a MusicBrainz release. --- beets/autotag/mb.py | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index c39f91c2d..0daa6c205 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -29,7 +29,9 @@ from beets import util from beets import config from collections import Counter from urllib.parse import urljoin -from beets.util.id_extractors import extract_discogs_id_regex +from beets.util.id_extractors import extract_discogs_id_regex, \ + spotify_id_regex, deezer_id_regex, beatport_id_regex +from beets.plugins import MetadataSourcePlugin VARIOUS_ARTISTS_ID = '89ad4ac3-39f7-470e-963a-56509c546377' @@ -512,14 +514,41 @@ def album_info(release: Dict) -> beets.autotag.hooks.AlbumInfo: in sorted(genres.items(), key=lambda g: -g[1]) ) - # We might find URLs to external sources (Discogs, Spotify, ...) + # We might find links to external sources (Discogs, Bandcamp, ...) if release.get('url-relation-list'): - discogs_url = None + discogs_url, bandcamp_url, spotify_url = None, None, None + deezer_url, beatport_url = None, None + for url in release['url-relation-list']: if url['type'] == 'discogs': log.debug('Found link to Discogs release via MusicBrainz') discogs_url = url['target'] - info.discogs_albumid = extract_release_id_regex(discogs_url) + if 'bandcamp.com' in url['target']: + log.debug('Found link to Bandcamp release via MusicBrainz') + bandcamp_url = url['target'] + if 'spotify.com' in url['target']: + log.debug('Found link to Spotify album via MusicBrainz') + spotify_url = url['target'] + if 'deezer.com' in url['target']: + log.debug('Found link to Deezer album via MusicBrainz') + deezer_url = url['target'] + if 'beatport.com' in url['target']: + log.debug('Found link to Beatport release via MusicBrainz') + beatport_url = url['target'] + + if discogs_url: + info.discogs_albumid = extract_discogs_id_regex(discogs_url) + if bandcamp_url: + info.bandcamp_album_id = bandcamp_url + if spotify_url: + info.spotify_album_id = MetadataSourcePlugin._get_id( + 'album', spotify_url, spotify_id_regex) + if deezer_url: + info.deezer_album_id = MetadataSourcePlugin._get_id( + 'album', deezer_url, deezer_id_regex) + if beatport_url: + info.beatport_album_id = MetadataSourcePlugin._get_id( + 'album', beatport_url, beatport_id_regex) extra_albumdatas = plugins.send('mb_album_extract', data=release) for extra_albumdata in extra_albumdatas: From fd4cecc29e24da70d50fb576ae3eadcda0353da8 Mon Sep 17 00:00:00 2001 From: J0J0 Todos Date: Sun, 12 Mar 2023 15:56:05 +0100 Subject: [PATCH 12/68] Make external IDs fetching via MB configurable - By default no external URLs will be looked for in the 'url-relation-list' of the releae dictionary. - Enabling is possible per each external service. --- beets/autotag/mb.py | 26 ++++++++++++++++++++------ beets/config_default.yaml | 6 ++++++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index 0daa6c205..cee2bdfd9 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -515,24 +515,38 @@ def album_info(release: Dict) -> beets.autotag.hooks.AlbumInfo: ) # We might find links to external sources (Discogs, Bandcamp, ...) - if release.get('url-relation-list'): + if (any(config['musicbrainz']['external_ids'].get().values()) + and release.get('url-relation-list')): discogs_url, bandcamp_url, spotify_url = None, None, None deezer_url, beatport_url = None, None + fetch_discogs, fetch_bandcamp, fetch_spotify = False, False, False + fetch_deezer, fetch_beatport = False, False + + if config['musicbrainz']['external_ids']['discogs'].get(): + fetch_discogs = True + if config['musicbrainz']['external_ids']['bandcamp'].get(): + fetch_bandcamp = True + if config['musicbrainz']['external_ids']['spotify'].get(): + fetch_spotify = True + if config['musicbrainz']['external_ids']['deezer'].get(): + fetch_deezer = True + if config['musicbrainz']['external_ids']['beatport'].get(): + fetch_beatport = True for url in release['url-relation-list']: - if url['type'] == 'discogs': + if fetch_discogs and url['type'] == 'discogs': log.debug('Found link to Discogs release via MusicBrainz') discogs_url = url['target'] - if 'bandcamp.com' in url['target']: + if fetch_bandcamp and 'bandcamp.com' in url['target']: log.debug('Found link to Bandcamp release via MusicBrainz') bandcamp_url = url['target'] - if 'spotify.com' in url['target']: + if fetch_spotify and 'spotify.com' in url['target']: log.debug('Found link to Spotify album via MusicBrainz') spotify_url = url['target'] - if 'deezer.com' in url['target']: + if fetch_deezer and 'deezer.com' in url['target']: log.debug('Found link to Deezer album via MusicBrainz') deezer_url = url['target'] - if 'beatport.com' in url['target']: + if fetch_beatport and 'beatport.com' in url['target']: log.debug('Found link to Beatport release via MusicBrainz') beatport_url = url['target'] diff --git a/beets/config_default.yaml b/beets/config_default.yaml index 2798b3872..6dcadccb2 100644 --- a/beets/config_default.yaml +++ b/beets/config_default.yaml @@ -128,6 +128,12 @@ musicbrainz: searchlimit: 5 extra_tags: [] genres: no + external_ids: + discogs: no + bandcamp: no + spotify: no + deezer: no + beatport: no match: strong_rec_thresh: 0.04 From 6acee803f7e74ccc656f33e79c7527f07a31c1ca Mon Sep 17 00:00:00 2001 From: J0J0 Todos Date: Thu, 3 Nov 2022 20:37:54 +0100 Subject: [PATCH 13/68] Exclude ID's when preserving flexible attributes - Bandcamp, Spotify, Deezer and Beatport ID's are saved in the library as flexible attributes. - On _reimports_ the method importer.ImportTask.reimport_metadata() takes care of preserving existing values for flexible attributes instead of applying new (and potentially empty) values. - In this case we don't want this behaviour and need to make sure that new values are applied. Therefore we check whether such ID's of metadata services are present in the reimported items and exclude them in reimport_metadata(). --- beets/importer.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/beets/importer.py b/beets/importer.py index c0319fc96..feebadc09 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -839,6 +839,19 @@ class ImportTask(BaseImportTask): dup_item.id, displayable_path(item.path) ) + # We exclude certain flexible attributes from the preserving + # process since they might have been fetched from MusicBrainz + # and been set in beets.autotag.apply_metadata(). + # discogs_albumid might also have been set but is not a + # flexible attribute, thus no exclude is required. + if item.get('bandcamp_album_id'): + dup_item.bandcamp_album_id = item.bandcamp_album_id + if item.get('spotify_album_id'): + dup_item.spotify_album_id = item.spotify_album_id + if item.get('deezer_album_id'): + dup_item.deezer_album_id = item.deezer_album_id + if item.get('beatport_album_id'): + dup_item.beatport_album_id = item.beatport_album_id item.update(dup_item._values_flex) log.debug( 'Reimported item flexible attributes {0} ' From 348a9fbf13a4ca144b97d4c8f9b56f78c81d812f Mon Sep 17 00:00:00 2001 From: J0J0 Todos Date: Fri, 4 Nov 2022 16:51:40 +0100 Subject: [PATCH 14/68] Changelog for #4708 --- docs/changelog.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9071f8831..435c85709 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -63,6 +63,11 @@ New features: * :doc:`/plugins/fromfilename`: Add debug log messages that inform when the plugin replaced bad (missing) artist, title or tracknumber metadata. :bug:`4561` :bug:`4600` +* :ref:`musicbrainz-config`: MusicBrainz release pages often link to related + metadata sources like Discogs, Bandcamp, Spotify, Deezer and Beatport. When + enabled via the :ref:`musicbrainz.external_ids` options, release ID's will be + extracted from those URL's and imported to the library. + :bug:`4220` Bug fixes: From 2e5394246fafcc1a41b43fb75c24c47a1b95620c Mon Sep 17 00:00:00 2001 From: J0J0 Todos Date: Fri, 4 Nov 2022 17:51:27 +0100 Subject: [PATCH 15/68] Docs for #4708 --- docs/plugins/index.rst | 2 ++ docs/reference/config.rst | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index ea13d2feb..4256ad690 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -129,6 +129,8 @@ following to your configuration:: web zero +.. _autotagger_extensions: + Autotagger Extensions --------------------- diff --git a/docs/reference/config.rst b/docs/reference/config.rst index b6fa8fea6..f162c6762 100644 --- a/docs/reference/config.rst +++ b/docs/reference/config.rst @@ -842,6 +842,32 @@ release and the release-group on MusicBrainz, separated by "; " and sorted by the total number of votes. Default: ``no`` +.. _musicbrainz.external_ids: + +external_ids +~~~~~~~~~~~~ + +Set any of the ``external_ids`` options to ``yes`` to enable the MusicBrainz +importer to look for links to related metadata sources. If such a link is +available the release ID will be extracted from the URL provided and imported +to the beets library. + + musicbrainz: + external_ids: + discogs: yes + spotify: yes + bandcamp: yes + beatport: yes + deezer: yes + + +The library fields of the corresponding :ref:`autotagger_extensions` are used +to save the data (``discogs_albumid``, ``bandcamp_album_id``, +``spotify_album_id``, ``beatport_album_id``, ``deezer_album_id``). On +re-imports existing data will be overwritten. + +The default of all options is ``no``. + .. _match-config: Autotagger Matching Options From e5f2e6d37bb5fc888013f19f4900babc0b500607 Mon Sep 17 00:00:00 2001 From: Andrew Rogl Date: Sun, 26 Mar 2023 18:35:29 +1000 Subject: [PATCH 16/68] Updated comments to attempt to be more self contained --- beets/importer.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/beets/importer.py b/beets/importer.py index 5c65b57c1..c8542ba82 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -1095,11 +1095,10 @@ class ArchiveImportTask(SentinelImportTask): # https://stackoverflow.com/q/9813243 for f in archive.infolist(): - # still need to adjust the dt o/w item will have the current dt - # update date_time - # Can you give a clarification why you add (0, 0, -1) to the date_time? - # Is the current a second off? - # (0, 0, -1) is added because time.mktime expects a 9-element tuple. + # The date_time will need to adjusted otherwise + # the item will have the current date_time of extraction. + # The (0, 0, -1) is added to date_time because the + # function time.mktime expects a 9-element tuple. # The -1 indicates that the DST flag is unknown. date_time = time.mktime(f.date_time + (0, 0, -1)) From 5bf4e3d92f5b4010a19cd3e8222fbf28b7b844e0 Mon Sep 17 00:00:00 2001 From: J0J0 Todos Date: Sun, 26 Mar 2023 13:10:28 +0200 Subject: [PATCH 17/68] Dedicated flex attrs for Deezer, Beatport album ID - Similar to what the Spotify plugin does, on imports we save to a field `..._album_id` (spotify_album_id, deezer_album_id, beatport_album_id) - It would be good to submit such a change to the 3rd-party plugins beetcamp and beatport4 as well (beatport_album_id, bandcamp_album_id). - We might need to investigate why none of these flex attr fields get populated to the beets album level (`beet info -a`, album_attributes db table), it is only available at the item level (`beet info`, item_attributes db table). This should be tackled in a future issue/PR. --- beetsplug/beatport.py | 1 + beetsplug/deezer.py | 1 + 2 files changed, 2 insertions(+) diff --git a/beetsplug/beatport.py b/beetsplug/beatport.py index eabf5dc31..bede8071d 100644 --- a/beetsplug/beatport.py +++ b/beetsplug/beatport.py @@ -432,6 +432,7 @@ class BeatportPlugin(BeetsPlugin): tracks = [self._get_track_info(x) for x in release.tracks] return AlbumInfo(album=release.name, album_id=release.beatport_id, + beatport_album_id=release.beatport_id, artist=artist, artist_id=artist_id, tracks=tracks, albumtype=release.category, va=va, year=release.release_date.year, diff --git a/beetsplug/deezer.py b/beetsplug/deezer.py index 86c182c39..77f7edc85 100644 --- a/beetsplug/deezer.py +++ b/beetsplug/deezer.py @@ -98,6 +98,7 @@ class DeezerPlugin(MetadataSourcePlugin, BeetsPlugin): return AlbumInfo( album=album_data['title'], album_id=deezer_id, + deezer_album_id=deezer_id, artist=artist, artist_credit=self.get_artist([album_data['artist']])[0], artist_id=artist_id, From c59ca0fca1fb292778ec89fc47a9c358ec902087 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 26 Mar 2023 14:13:20 -0400 Subject: [PATCH 18/68] enforced utf-8 encoding on imported files --- beets/ui/commands.py | 2 +- docs/reference/cli.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 6c8e25b85..68cc7b635 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -958,7 +958,7 @@ def import_files(lib, paths, query): if config['import']['log'].get() is not None: logpath = syspath(config['import']['log'].as_filename()) try: - loghandler = logging.FileHandler(logpath) + loghandler = logging.FileHandler(logpath, encoding='utf-8') except OSError: raise ui.UserError("could not open log file for writing: " "{}".format(displayable_path(logpath))) diff --git a/docs/reference/cli.rst b/docs/reference/cli.rst index da119d0f8..7737c4498 100644 --- a/docs/reference/cli.rst +++ b/docs/reference/cli.rst @@ -172,7 +172,7 @@ Optional command flags: Just point the ``beet import`` command at a directory of files that are already catalogged in your library. Beets will automatically detect this - situation and avoid duplicating any items. In this situation, the "copy + situation and avoid duplicating any items. A UTF-8 encoding will be enforced on your imported file. In this situation, the "copy files" option (``-c``/``-C`` on the command line or ``copy`` in the config file) has slightly different behavior: it causes files to be *moved*, rather than duplicated, if they're already in your library. (The same is From 6539cdcfc5068834b3d226e8e8e72c819708996d Mon Sep 17 00:00:00 2001 From: root Date: Sun, 26 Mar 2023 14:20:10 -0400 Subject: [PATCH 19/68] added to changelog --- docs/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9071f8831..dc911f540 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,6 +11,8 @@ for Python 3.6). New features: +* Added UTF-8 encoding enforcement to imported files in `beets/beets/ui/commands.py`. + :bug:`4693` * Added additional error handling for `spotify` plugin. :bug:`4686` * We now import the remixer field from Musicbrainz into the library. From 4e519b200dd4f02746af40c501c84218d9ad9dd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Mon, 27 Mar 2023 02:27:32 +0100 Subject: [PATCH 20/68] Update beetcamp URL in id_extractors --- beets/util/id_extractors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/util/id_extractors.py b/beets/util/id_extractors.py index b1020e78c..93fc2056c 100644 --- a/beets/util/id_extractors.py +++ b/beets/util/id_extractors.py @@ -35,7 +35,7 @@ beatport_id_regex = { # A note on Bandcamp: There is no such thing as a Bandcamp album or artist ID, # the URL can be used as the identifier. The Bandcamp metadata source plugin -# works that way - https://github.com/unrblt/beets-bandcamp. Bandcamp album +# works that way - https://github.com/snejus/beetcamp. Bandcamp album # URLs usually look like: https://nameofartist.bandcamp.com/album/nameofalbum From 861bc69df55cb3d4d20da6082878542ff42d85d8 Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Sat, 18 Jun 2022 06:36:20 +0200 Subject: [PATCH 21/68] convert: Add a quick & dirty m3u playlist feature --- beetsplug/convert.py | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 6ed6b6e54..1f6682474 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -149,6 +149,7 @@ class ConvertPlugin(BeetsPlugin): 'copy_album_art': False, 'album_art_maxwidth': 0, 'delete_originals': False, + 'playlist': '', }) self.early_import_stages = [self.auto_convert, self.auto_convert_keep] @@ -177,6 +178,8 @@ class ConvertPlugin(BeetsPlugin): dest='hardlink', help='hardlink files that do not \ need transcoding. Overrides --link.') + cmd.parser.add_option('-m', '--playlist', action='store', + help='set the name of the playlist file to be created') cmd.parser.add_album_option() cmd.func = self.convert_func return [cmd] @@ -257,7 +260,7 @@ class ConvertPlugin(BeetsPlugin): util.displayable_path(source)) def convert_item(self, dest_dir, keep_new, path_formats, fmt, - pretend=False, link=False, hardlink=False): + pretend=False, link=False, hardlink=False, playlist=''): """A pipeline thread that converts `Item` objects from a library. """ @@ -282,6 +285,13 @@ class ConvertPlugin(BeetsPlugin): dest = replace_ext(dest, ext) converted = dest + # Quick, dirty, playlist + # converted_filename = Path(str(converted)).name + converted_filename = os.path.basename(converted) + self._log.info("Appending to playlist file {0}".format(playlist)) + with open(playlist, "ab") as playlist_file: + playlist_file.write(converted_filename + b"\n") + # Ensure that only one thread tries to create directories at a # time. (The existence check is not atomic with the directory # creation inside this function.) @@ -436,7 +446,7 @@ class ConvertPlugin(BeetsPlugin): def convert_func(self, lib, opts, args): (dest, threads, path_formats, fmt, - pretend, hardlink, link) = self._get_opts_and_config(opts) + pretend, hardlink, link, playlist) = self._get_opts_and_config(opts) if opts.album: albums = lib.albums(ui.decargs(args)) @@ -461,8 +471,16 @@ class ConvertPlugin(BeetsPlugin): self.copy_album_art(album, dest, path_formats, pretend, link, hardlink) + if playlist: + print("################") + print(f"Playlist: {playlist}") + print("################") + if not pretend: + with open(playlist, "w") as playlist_file: + playlist_file.write("#EXTM3U" + "\n") + self._parallel_convert(dest, opts.keep_new, path_formats, fmt, - pretend, link, hardlink, threads, items) + pretend, link, hardlink, threads, items, playlist) def convert_on_import(self, lib, item): """Transcode a file automatically after it is imported into the @@ -544,6 +562,8 @@ class ConvertPlugin(BeetsPlugin): fmt = opts.format or self.config['format'].as_str().lower() + playlist = opts.playlist or self.config['playlist'].get() + if opts.pretend is not None: pretend = opts.pretend else: @@ -559,10 +579,11 @@ class ConvertPlugin(BeetsPlugin): hardlink = self.config['hardlink'].get(bool) link = self.config['link'].get(bool) - return dest, threads, path_formats, fmt, pretend, hardlink, link + + return dest, threads, path_formats, fmt, pretend, hardlink, link, playlist def _parallel_convert(self, dest, keep_new, path_formats, fmt, - pretend, link, hardlink, threads, items): + pretend, link, hardlink, threads, items, playlist): """Run the convert_item function for every items on as many thread as defined in threads """ @@ -572,7 +593,8 @@ class ConvertPlugin(BeetsPlugin): fmt, pretend, link, - hardlink) + hardlink, + playlist) for _ in range(threads)] pipe = util.pipeline.Pipeline([iter(items), convert]) pipe.run_parallel() From d448e0c4de6f5f3d81ad9a2de874a515d6debeb8 Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Mon, 4 Jul 2022 14:44:35 +0200 Subject: [PATCH 22/68] convert: Refine and fix playlist feature - Improve --help text - Use unicode instead of bytes when adding media file paths to the playlist file. - The "standard" (?) of m3u8 defines that unicode should ensure support of special characters in media file names. util.displayable_path() is used to do the conversion from bytes. We save everything in bytes in the config since it seemes to be the way this plugin or beets in general likes to save paths. - Join dest and playlist in the config reader method already to have it ready in both methods that require the full path to the playlist file. --- beetsplug/convert.py | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 1f6682474..35f5b3c1c 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -149,7 +149,7 @@ class ConvertPlugin(BeetsPlugin): 'copy_album_art': False, 'album_art_maxwidth': 0, 'delete_originals': False, - 'playlist': '', + 'playlist': None, }) self.early_import_stages = [self.auto_convert, self.auto_convert_keep] @@ -179,7 +179,14 @@ class ConvertPlugin(BeetsPlugin): help='hardlink files that do not \ need transcoding. Overrides --link.') cmd.parser.add_option('-m', '--playlist', action='store', - help='set the name of the playlist file to be created') + help='''the name of an m3u8 playlist file to + be created in the root of the destination folder. + The m3u8 format ensures special characters + support by using unicode to save media file + paths. Relative paths are used to point to media + files ensuring a working playlist when + transferred to a different computer (eg. when + opened from an external drive).''') cmd.parser.add_album_option() cmd.func = self.convert_func return [cmd] @@ -260,7 +267,7 @@ class ConvertPlugin(BeetsPlugin): util.displayable_path(source)) def convert_item(self, dest_dir, keep_new, path_formats, fmt, - pretend=False, link=False, hardlink=False, playlist=''): + pretend=False, link=False, hardlink=False, playlist=None): """A pipeline thread that converts `Item` objects from a library. """ @@ -285,12 +292,18 @@ class ConvertPlugin(BeetsPlugin): dest = replace_ext(dest, ext) converted = dest - # Quick, dirty, playlist - # converted_filename = Path(str(converted)).name - converted_filename = os.path.basename(converted) - self._log.info("Appending to playlist file {0}".format(playlist)) - with open(playlist, "ab") as playlist_file: - playlist_file.write(converted_filename + b"\n") + # When the playlist argument is passed, add the current filename to + # an m3u8 playlist file located in the destination folder. + if playlist: + self._log.debug( + "Appending to playlist file {0}", + util.displayable_path(playlist) + ) + with open(playlist, "a") as playlist_file: + # The classic m3u format doesn't support special characters + # in media file paths, thus we use the m3u8 format which + # requires media file paths to be unicode. + playlist_file.write(util.displayable_path(dest) + "\n") # Ensure that only one thread tries to create directories at a # time. (The existence check is not atomic with the directory @@ -472,11 +485,9 @@ class ConvertPlugin(BeetsPlugin): link, hardlink) if playlist: - print("################") - print(f"Playlist: {playlist}") - print("################") + self._log.info("Creating playlist file: {0}", playlist) if not pretend: - with open(playlist, "w") as playlist_file: + with open(os.path.join(dest, playlist), "w") as playlist_file: playlist_file.write("#EXTM3U" + "\n") self._parallel_convert(dest, opts.keep_new, path_formats, fmt, @@ -563,6 +574,8 @@ class ConvertPlugin(BeetsPlugin): fmt = opts.format or self.config['format'].as_str().lower() playlist = opts.playlist or self.config['playlist'].get() + if playlist is not None: + playlist = os.path.join(dest, util.bytestring_path(playlist)) if opts.pretend is not None: pretend = opts.pretend From fd8fe69738c1b2e56d4650364ecd4e90dea3f0af Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Mon, 4 Jul 2022 15:09:31 +0200 Subject: [PATCH 23/68] convert: Playlist feature linting fixes --- beetsplug/convert.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 35f5b3c1c..2680d3951 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -490,8 +490,8 @@ class ConvertPlugin(BeetsPlugin): with open(os.path.join(dest, playlist), "w") as playlist_file: playlist_file.write("#EXTM3U" + "\n") - self._parallel_convert(dest, opts.keep_new, path_formats, fmt, - pretend, link, hardlink, threads, items, playlist) + self._parallel_convert(dest, opts.keep_new, path_formats, fmt, pretend, + link, hardlink, threads, items, playlist) def convert_on_import(self, lib, item): """Transcode a file automatically after it is imported into the @@ -592,8 +592,8 @@ class ConvertPlugin(BeetsPlugin): hardlink = self.config['hardlink'].get(bool) link = self.config['link'].get(bool) - - return dest, threads, path_formats, fmt, pretend, hardlink, link, playlist + return (dest, threads, path_formats, fmt, pretend, hardlink, link, + playlist) def _parallel_convert(self, dest, keep_new, path_formats, fmt, pretend, link, hardlink, threads, items, playlist): From 16e25bb61bd821fb28f602b3aca27f4ec3f5ee9a Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Mon, 4 Jul 2022 16:14:33 +0200 Subject: [PATCH 24/68] convert: playlist feature: Fix relative paths pointing to media files in playlist. Also refine code comment and move to a better fitting place. --- beetsplug/convert.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 2680d3951..082124b8c 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -299,11 +299,16 @@ class ConvertPlugin(BeetsPlugin): "Appending to playlist file {0}", util.displayable_path(playlist) ) + # The classic m3u format doesn't support special characters in + # media file paths, thus we use the m3u8 format which requires + # media file paths to be unicode. Additionally we use relative + # paths to ensure readability of the playlist on remote + # computers. + dest_relative = util.displayable_path(dest).replace( + util.displayable_path(dest_dir) + os.sep, "" + ) with open(playlist, "a") as playlist_file: - # The classic m3u format doesn't support special characters - # in media file paths, thus we use the m3u8 format which - # requires media file paths to be unicode. - playlist_file.write(util.displayable_path(dest) + "\n") + playlist_file.write(dest_relative + "\n") # Ensure that only one thread tries to create directories at a # time. (The existence check is not atomic with the directory From c0b1bc986705d75a36a90da4f7999b39fcf420c0 Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Wed, 6 Jul 2022 08:36:32 +0200 Subject: [PATCH 25/68] convert: playlist feature: Better relative path gen Use Item.destination method for generation of relative paths to media files in playlist. The fragment keyword enables returning the path as unicode instead of bytes, let's keep that in mind. --- beetsplug/convert.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 082124b8c..5d6f2ce07 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -304,8 +304,10 @@ class ConvertPlugin(BeetsPlugin): # media file paths to be unicode. Additionally we use relative # paths to ensure readability of the playlist on remote # computers. - dest_relative = util.displayable_path(dest).replace( - util.displayable_path(dest_dir) + os.sep, "" + dest_relative = item.destination( + basedir=dest_dir, + path_formats=path_formats, + fragment=True ) with open(playlist, "a") as playlist_file: playlist_file.write(dest_relative + "\n") From d589e77adea920e47c5ddc6123de2945f8d993c3 Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Sun, 10 Jul 2022 12:25:51 +0200 Subject: [PATCH 26/68] convert: playlist: Fix redundant join path It's done in _get_opts_and_config already. --- beetsplug/convert.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 5d6f2ce07..734f393bc 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -494,7 +494,7 @@ class ConvertPlugin(BeetsPlugin): if playlist: self._log.info("Creating playlist file: {0}", playlist) if not pretend: - with open(os.path.join(dest, playlist), "w") as playlist_file: + with open(playlist, "w") as playlist_file: playlist_file.write("#EXTM3U" + "\n") self._parallel_convert(dest, opts.keep_new, path_formats, fmt, pretend, From c251ed19c4162803e4de74bce4b0d792816ef0e9 Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Sun, 10 Jul 2022 15:18:42 +0200 Subject: [PATCH 27/68] convert: playlist: Generate m3u file in one batch to avoid any possible interference with the threaded file conversion mechanism. --- beetsplug/convert.py | 44 ++++++++++++++++++-------------------------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 734f393bc..ebac336d5 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -267,7 +267,7 @@ class ConvertPlugin(BeetsPlugin): util.displayable_path(source)) def convert_item(self, dest_dir, keep_new, path_formats, fmt, - pretend=False, link=False, hardlink=False, playlist=None): + pretend=False, link=False, hardlink=False): """A pipeline thread that converts `Item` objects from a library. """ @@ -292,26 +292,6 @@ class ConvertPlugin(BeetsPlugin): dest = replace_ext(dest, ext) converted = dest - # When the playlist argument is passed, add the current filename to - # an m3u8 playlist file located in the destination folder. - if playlist: - self._log.debug( - "Appending to playlist file {0}", - util.displayable_path(playlist) - ) - # The classic m3u format doesn't support special characters in - # media file paths, thus we use the m3u8 format which requires - # media file paths to be unicode. Additionally we use relative - # paths to ensure readability of the playlist on remote - # computers. - dest_relative = item.destination( - basedir=dest_dir, - path_formats=path_formats, - fragment=True - ) - with open(playlist, "a") as playlist_file: - playlist_file.write(dest_relative + "\n") - # Ensure that only one thread tries to create directories at a # time. (The existence check is not atomic with the directory # creation inside this function.) @@ -492,13 +472,26 @@ class ConvertPlugin(BeetsPlugin): link, hardlink) if playlist: + # When playlist arg is passed create an m3u8 file in dest folder. + # + # The classic m3u format doesn't support special characters in + # media file paths, thus we use the m3u8 format which requires + # media file paths to be unicode. Additionally we use relative + # paths to ensure readability of the playlist on remote + # computers. self._log.info("Creating playlist file: {0}", playlist) + items_paths = [ + item.destination( + basedir=dest, path_formats=path_formats, fragment=True + ) for item in items + ] + items_paths = ["#EXTM3U"] + items_paths if not pretend: with open(playlist, "w") as playlist_file: - playlist_file.write("#EXTM3U" + "\n") + playlist_file.writelines('\n'.join(items_paths)) self._parallel_convert(dest, opts.keep_new, path_formats, fmt, pretend, - link, hardlink, threads, items, playlist) + link, hardlink, threads, items) def convert_on_import(self, lib, item): """Transcode a file automatically after it is imported into the @@ -603,7 +596,7 @@ class ConvertPlugin(BeetsPlugin): playlist) def _parallel_convert(self, dest, keep_new, path_formats, fmt, - pretend, link, hardlink, threads, items, playlist): + pretend, link, hardlink, threads, items): """Run the convert_item function for every items on as many thread as defined in threads """ @@ -613,8 +606,7 @@ class ConvertPlugin(BeetsPlugin): fmt, pretend, link, - hardlink, - playlist) + hardlink) for _ in range(threads)] pipe = util.pipeline.Pipeline([iter(items), convert]) pipe.run_parallel() From 5dfff5000585cb987817c8b11c9ae2d48492fd06 Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Wed, 13 Jul 2022 08:49:53 +0200 Subject: [PATCH 28/68] convert: playlist: Refactor m3u writing to class and also implement a currently untested load() method. --- beets/util/__init__.py | 50 ++++++++++++++++++++++++++++++++++++++++++ beetsplug/convert.py | 7 +++--- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 2319890a3..3d27edd5e 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -135,6 +135,56 @@ class MoveOperation(Enum): REFLINK_AUTO = 5 +class M3UFile(): + def __init__(self, path): + """Reads and writes m3u or m3u8 playlist files. + + ``path`` is the full path to the playlist file. + + The playlist file type, m3u or m3u8 is determined by 1) the ending + being m3u8 and 2) the file paths contained in the list being utf-8 + encoded. Since the list is passed from the outside, this is currently + out of control of this class. + """ + self.path = path + self.extm3u = False + self.media_list = [] + + def load(self): + """Reads the m3u file from disk and sets the object's attributes. + """ + with open(self.name, "r") as playlist_file: + raw_contents = playlist_file.readlines() + self.extm3u = True if raw_contents[0] == "#EXTM3U" else False + for line in raw_contents[1:]: + if line.startswith("#"): + # Some EXTM3U comment, do something. FIXME + continue + self.media_list.append(line) + + def set_contents(self, media_files, extm3u=True): + """Sets self.media_files to a list of media file paths, + + and sets additional flags, changing the final m3u-file's format. + + ``media_files`` is a list of paths to media files that should be added + to the playlist (relative or absolute paths, that's the responsibility + of the caller). By default the ``extm3u`` flag is set, to ensure a + save-operation writes an m3u-extended playlist (comment "#EXTM3U" at + the top of the file). + """ + self.media_files = media_files + self.extm3u = extm3u + + def write(self): + """Writes the m3u file to disk.""" + header = ["#EXTM3U"] if self.extm3u else [] + contents = header + self.media_files + with open(self.path, "w") as playlist_file: + playlist_file.writelines('\n'.join(contents)) + playlist_file.write('\n') # Final linefeed to prevent noeol file. + + def normpath(path): """Provide the canonical form of the path suitable for storing in the database. diff --git a/beetsplug/convert.py b/beetsplug/convert.py index ebac336d5..2856363ab 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -31,6 +31,7 @@ from beets import art from beets.util.artresizer import ArtResizer from beets.library import parse_query_string from beets.library import Item +from beets.util import M3UFile _fs_lock = threading.Lock() _temp_files = [] # Keep track of temporary transcoded files for deletion. @@ -485,10 +486,10 @@ class ConvertPlugin(BeetsPlugin): basedir=dest, path_formats=path_formats, fragment=True ) for item in items ] - items_paths = ["#EXTM3U"] + items_paths if not pretend: - with open(playlist, "w") as playlist_file: - playlist_file.writelines('\n'.join(items_paths)) + m3ufile = M3UFile(playlist) + m3ufile.set_contents(items_paths) + m3ufile.write() self._parallel_convert(dest, opts.keep_new, path_formats, fmt, pretend, link, hardlink, threads, items) From 9930a5da5939acc62d6fe64d949dc11b3777331b Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Wed, 10 Aug 2022 07:17:56 +0200 Subject: [PATCH 29/68] convert: playlist: Test playlist existence Broken commit around writing a unittest that checks for existence of an m3u file after convert has been called with --playlist option. --- test/test_convert.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/test/test_convert.py b/test/test_convert.py index 7cdef4627..f114a04dc 100644 --- a/test/test_convert.py +++ b/test/test_convert.py @@ -184,8 +184,9 @@ class ConvertCliTest(unittest.TestCase, TestHelper, ConvertCommand): } def tearDown(self): - self.unload_plugins() - self.teardown_beets() + pass + #self.unload_plugins() + #self.teardown_beets() def test_convert(self): with control_stdin('y'): @@ -293,6 +294,19 @@ class ConvertCliTest(unittest.TestCase, TestHelper, ConvertCommand): converted = os.path.join(self.convert_dest, b'converted.ogg') self.assertNoFileTag(converted, 'ogg') + def test_playlist(self): + with control_stdin('y'): + self.run_convert('--playlist', 'playlist.m3u8') + converted = os.path.join(self.convert_dest, b'converted.mp3') + self.assertFileTag(converted, 'mp3') + m3u_created = os.path.join(self.convert_dest, b'playlist.m3u8') + self.assertTrue(os.path.exists(m3u_created)) + + def test_playlist_pretend(self): + self.run_convert('--playlist', 'playlist.m3u8', '--pretend') + m3u_created = os.path.join(self.convert_dest, b'playlist.m3u8') + self.assertFalse(os.path.exists(m3u_created)) + @_common.slow_test() class NeverConvertLossyFilesTest(unittest.TestCase, TestHelper, From e41525adbc1948683f66d7077a99664c460e87ee Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Fri, 12 Aug 2022 06:48:06 +0200 Subject: [PATCH 30/68] convert: playlist: Remove clutter from test - Remove comments in tearDown - Don't test for converted.mp3, it's done in a separate test already, we only want to test for the playlist file here. --- test/test_convert.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/test_convert.py b/test/test_convert.py index f114a04dc..950711f76 100644 --- a/test/test_convert.py +++ b/test/test_convert.py @@ -184,9 +184,8 @@ class ConvertCliTest(unittest.TestCase, TestHelper, ConvertCommand): } def tearDown(self): - pass - #self.unload_plugins() - #self.teardown_beets() + self.unload_plugins() + self.teardown_beets() def test_convert(self): with control_stdin('y'): @@ -297,8 +296,6 @@ class ConvertCliTest(unittest.TestCase, TestHelper, ConvertCommand): def test_playlist(self): with control_stdin('y'): self.run_convert('--playlist', 'playlist.m3u8') - converted = os.path.join(self.convert_dest, b'converted.mp3') - self.assertFileTag(converted, 'mp3') m3u_created = os.path.join(self.convert_dest, b'playlist.m3u8') self.assertTrue(os.path.exists(m3u_created)) From 55b386375ae0426fd4c0742644c230e25fdc4a7d Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Fri, 12 Aug 2022 06:55:20 +0200 Subject: [PATCH 31/68] convert: playlist: Move m3u creation after conversions - Move the creation of the playlist file to the very end, right after self._parallel_convert, in the convert plugin's main function. - In the test code, the destination directory is created when the conversion happens, thus this fixes test_playlist and doesn't hurt the feature - The playlist creation can as well be the very last step in the process. --- beetsplug/convert.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 2856363ab..ab6e4c420 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -472,6 +472,9 @@ class ConvertPlugin(BeetsPlugin): self.copy_album_art(album, dest, path_formats, pretend, link, hardlink) + self._parallel_convert(dest, opts.keep_new, path_formats, fmt, pretend, + link, hardlink, threads, items) + if playlist: # When playlist arg is passed create an m3u8 file in dest folder. # @@ -491,9 +494,6 @@ class ConvertPlugin(BeetsPlugin): m3ufile.set_contents(items_paths) m3ufile.write() - self._parallel_convert(dest, opts.keep_new, path_formats, fmt, pretend, - link, hardlink, threads, items) - def convert_on_import(self, lib, item): """Transcode a file automatically after it is imported into the library. From ba3740c8fe41b305d7ede50031e177f8ff2c66dd Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Fri, 12 Aug 2022 08:56:31 +0200 Subject: [PATCH 32/68] convert: playlist: Fix filename attr in load method --- beets/util/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 3d27edd5e..58eb47034 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -153,7 +153,7 @@ class M3UFile(): def load(self): """Reads the m3u file from disk and sets the object's attributes. """ - with open(self.name, "r") as playlist_file: + with open(self.path, "r") as playlist_file: raw_contents = playlist_file.readlines() self.extm3u = True if raw_contents[0] == "#EXTM3U" else False for line in raw_contents[1:]: From 0cbf91e4d8815f844cd560c7ab7e0e377ff8dd15 Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Fri, 12 Aug 2022 08:56:44 +0200 Subject: [PATCH 33/68] convert: playlist: Add test_m3ufile and fixtures Add several tests checking loading and saving unicode and regular ascii text playlist files. --- test/rsrc/playlist.m3u | 3 ++ test/rsrc/playlist.m3u8 | 3 ++ test/test_m3ufile.py | 81 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 test/rsrc/playlist.m3u create mode 100644 test/rsrc/playlist.m3u8 create mode 100644 test/test_m3ufile.py diff --git a/test/rsrc/playlist.m3u b/test/rsrc/playlist.m3u new file mode 100644 index 000000000..cd0cdaad5 --- /dev/null +++ b/test/rsrc/playlist.m3u @@ -0,0 +1,3 @@ +#EXTM3U +/This/is/a/path/to_a_file.mp3 +/This/is/another/path/to_a_file.mp3 diff --git a/test/rsrc/playlist.m3u8 b/test/rsrc/playlist.m3u8 new file mode 100644 index 000000000..a3b00eb6a --- /dev/null +++ b/test/rsrc/playlist.m3u8 @@ -0,0 +1,3 @@ +#EXTM3U +/This/is/å/path/to_a_file.mp3 +/This/is/another/path/tö_a_file.mp3 diff --git a/test/test_m3ufile.py b/test/test_m3ufile.py new file mode 100644 index 000000000..4f5807b6b --- /dev/null +++ b/test/test_m3ufile.py @@ -0,0 +1,81 @@ +# This file is part of beets. +# Copyright 2016, Johannes Tiefenbacher. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + + +from os import path, remove +from tempfile import mkdtemp +from shutil import rmtree +import unittest + +# from unittest.mock import Mock, MagicMock + +from beets.util import M3UFile +from beets.util import syspath, bytestring_path, py3_path, CHAR_REPLACE +from test._common import RSRC + + +class M3UFileTest(unittest.TestCase): + def test_playlist_write_empty(self): + tempdir = bytestring_path(mkdtemp()) + the_playlist_file = path.join(tempdir, b'playlist.m3u8') + m3ufile = M3UFile(the_playlist_file) + m3ufile.write() + self.assertFalse(path.exists(the_playlist_file)) + rmtree(tempdir) + + def test_playlist_write(self): + tempdir = bytestring_path(mkdtemp()) + the_playlist_file = path.join(tempdir, b'playlist.m3u') + m3ufile = M3UFile(the_playlist_file) + m3ufile.set_contents([ + '/This/is/a/path/to_a_file.mp3', + '/This/is/another/path/to_a_file.mp3', + ]) + m3ufile.write() + self.assertTrue(path.exists(the_playlist_file)) + rmtree(tempdir) + + def test_playlist_write_unicode(self): + tempdir = bytestring_path(mkdtemp()) + the_playlist_file = path.join(tempdir, b'playlist.m3u8') + m3ufile = M3UFile(the_playlist_file) + m3ufile.set_contents([ + '/This/is/å/path/to_a_file.mp3', + '/This/is/another/path/tö_a_file.mp3', + ]) + m3ufile.write() + self.assertTrue(path.exists(the_playlist_file)) + rmtree(tempdir) + + def test_playlist_load_ascii(self): + the_playlist_file = path.join(RSRC, b'playlist.m3u') + m3ufile = M3UFile(the_playlist_file) + m3ufile.load() + self.assertEqual(m3ufile.media_list[0], + '/This/is/a/path/to_a_file.mp3\n') + + def test_playlist_load_unicode(self): + the_playlist_file = path.join(RSRC, b'playlist.m3u8') + m3ufile = M3UFile(the_playlist_file) + m3ufile.load() + self.assertEqual(m3ufile.media_list[0], + '/This/is/å/path/to_a_file.mp3\n') + + +def suite(): + return unittest.TestLoader().loadTestsFromName(__name__) + + +if __name__ == '__main__': + unittest.main(defaultTest='suite') From 68240f6e03d91c622a53d02b90b0755673fd2e7f Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Fri, 12 Aug 2022 09:18:45 +0200 Subject: [PATCH 34/68] convert: playlist: Add EmptyPlaylistError and test - Add and Exception class called EmptyPlaylistError ought to be raised when playlists without files are loaded or saved. - Add a test for it in test_m3ufile - Fix media_files vs. media_list attribute name. --- beets/util/__init__.py | 21 ++++++++++++++++----- test/test_m3ufile.py | 6 +++--- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 58eb47034..9f0460ce1 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -124,6 +124,13 @@ class FilesystemError(HumanReadableException): return f'{self._reasonstr()} {clause}' +class EmptyPlaylistError(Exception): + """An error that should be raised when a playlist file without media files + is saved or loaded. + """ + pass + + class MoveOperation(Enum): """The file operations that e.g. various move functions can carry out. """ @@ -161,25 +168,29 @@ class M3UFile(): # Some EXTM3U comment, do something. FIXME continue self.media_list.append(line) + if not self.media_list: + raise EmptyPlaylistError - def set_contents(self, media_files, extm3u=True): - """Sets self.media_files to a list of media file paths, + def set_contents(self, media_list, extm3u=True): + """Sets self.media_list to a list of media file paths, and sets additional flags, changing the final m3u-file's format. - ``media_files`` is a list of paths to media files that should be added + ``media_list`` is a list of paths to media files that should be added to the playlist (relative or absolute paths, that's the responsibility of the caller). By default the ``extm3u`` flag is set, to ensure a save-operation writes an m3u-extended playlist (comment "#EXTM3U" at the top of the file). """ - self.media_files = media_files + self.media_list = media_list self.extm3u = extm3u def write(self): """Writes the m3u file to disk.""" header = ["#EXTM3U"] if self.extm3u else [] - contents = header + self.media_files + if not self.media_list: + raise EmptyPlaylistError + contents = header + self.media_list with open(self.path, "w") as playlist_file: playlist_file.writelines('\n'.join(contents)) playlist_file.write('\n') # Final linefeed to prevent noeol file. diff --git a/test/test_m3ufile.py b/test/test_m3ufile.py index 4f5807b6b..e0ab80fa0 100644 --- a/test/test_m3ufile.py +++ b/test/test_m3ufile.py @@ -20,7 +20,7 @@ import unittest # from unittest.mock import Mock, MagicMock -from beets.util import M3UFile +from beets.util import M3UFile, EmptyPlaylistError from beets.util import syspath, bytestring_path, py3_path, CHAR_REPLACE from test._common import RSRC @@ -30,8 +30,8 @@ class M3UFileTest(unittest.TestCase): tempdir = bytestring_path(mkdtemp()) the_playlist_file = path.join(tempdir, b'playlist.m3u8') m3ufile = M3UFile(the_playlist_file) - m3ufile.write() - self.assertFalse(path.exists(the_playlist_file)) + with self.assertRaises(EmptyPlaylistError): + m3ufile.write() rmtree(tempdir) def test_playlist_write(self): From 39e4b90b5c8c947950cc54beb0f197cc216724bd Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Fri, 12 Aug 2022 09:33:09 +0200 Subject: [PATCH 35/68] convert: playlist: Add tests checking extm3u and fix extm3u check in load method. --- beets/util/__init__.py | 2 +- test/rsrc/playlist_non_ext.m3u | 2 ++ test/test_m3ufile.py | 12 ++++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 test/rsrc/playlist_non_ext.m3u diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 9f0460ce1..e28039927 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -162,7 +162,7 @@ class M3UFile(): """ with open(self.path, "r") as playlist_file: raw_contents = playlist_file.readlines() - self.extm3u = True if raw_contents[0] == "#EXTM3U" else False + self.extm3u = True if raw_contents[0] == "#EXTM3U\n" else False for line in raw_contents[1:]: if line.startswith("#"): # Some EXTM3U comment, do something. FIXME diff --git a/test/rsrc/playlist_non_ext.m3u b/test/rsrc/playlist_non_ext.m3u new file mode 100644 index 000000000..a2d179010 --- /dev/null +++ b/test/rsrc/playlist_non_ext.m3u @@ -0,0 +1,2 @@ +/This/is/a/path/to_a_file.mp3 +/This/is/another/path/to_a_file.mp3 diff --git a/test/test_m3ufile.py b/test/test_m3ufile.py index e0ab80fa0..a3f8703b5 100644 --- a/test/test_m3ufile.py +++ b/test/test_m3ufile.py @@ -72,6 +72,18 @@ class M3UFileTest(unittest.TestCase): self.assertEqual(m3ufile.media_list[0], '/This/is/å/path/to_a_file.mp3\n') + def test_playlist_load_extm3u(self): + the_playlist_file = path.join(RSRC, b'playlist.m3u') + m3ufile = M3UFile(the_playlist_file) + m3ufile.load() + self.assertTrue(m3ufile.extm3u) + + def test_playlist_load_non_extm3u(self): + the_playlist_file = path.join(RSRC, b'playlist_non_ext.m3u') + m3ufile = M3UFile(the_playlist_file) + m3ufile.load() + self.assertFalse(m3ufile.extm3u) + def suite(): return unittest.TestLoader().loadTestsFromName(__name__) From 01b77f5602636a45d9f32a2d70a422469eb4725e Mon Sep 17 00:00:00 2001 From: J0J0 Todos Date: Wed, 29 Mar 2023 07:45:51 +0200 Subject: [PATCH 36/68] convert: playlist: Add changelog entry --- docs/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 435c85709..78bbb249e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -68,6 +68,9 @@ New features: enabled via the :ref:`musicbrainz.external_ids` options, release ID's will be extracted from those URL's and imported to the library. :bug:`4220` +* :doc:`/plugins/convert`: Add support for generating m3u8 playlists together + with converted media files. + :bug:`4373` Bug fixes: From 7d121c390b3fb36717571c72c353c2a5090c8242 Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Mon, 22 Aug 2022 10:46:12 +0200 Subject: [PATCH 37/68] convert: playlist: Move M3UFile class to separate module in util package. --- beets/util/__init__.py | 61 --------------------------------- beets/util/m3u.py | 77 ++++++++++++++++++++++++++++++++++++++++++ beetsplug/convert.py | 2 +- 3 files changed, 78 insertions(+), 62 deletions(-) create mode 100644 beets/util/m3u.py diff --git a/beets/util/__init__.py b/beets/util/__init__.py index e28039927..2319890a3 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -124,13 +124,6 @@ class FilesystemError(HumanReadableException): return f'{self._reasonstr()} {clause}' -class EmptyPlaylistError(Exception): - """An error that should be raised when a playlist file without media files - is saved or loaded. - """ - pass - - class MoveOperation(Enum): """The file operations that e.g. various move functions can carry out. """ @@ -142,60 +135,6 @@ class MoveOperation(Enum): REFLINK_AUTO = 5 -class M3UFile(): - def __init__(self, path): - """Reads and writes m3u or m3u8 playlist files. - - ``path`` is the full path to the playlist file. - - The playlist file type, m3u or m3u8 is determined by 1) the ending - being m3u8 and 2) the file paths contained in the list being utf-8 - encoded. Since the list is passed from the outside, this is currently - out of control of this class. - """ - self.path = path - self.extm3u = False - self.media_list = [] - - def load(self): - """Reads the m3u file from disk and sets the object's attributes. - """ - with open(self.path, "r") as playlist_file: - raw_contents = playlist_file.readlines() - self.extm3u = True if raw_contents[0] == "#EXTM3U\n" else False - for line in raw_contents[1:]: - if line.startswith("#"): - # Some EXTM3U comment, do something. FIXME - continue - self.media_list.append(line) - if not self.media_list: - raise EmptyPlaylistError - - def set_contents(self, media_list, extm3u=True): - """Sets self.media_list to a list of media file paths, - - and sets additional flags, changing the final m3u-file's format. - - ``media_list`` is a list of paths to media files that should be added - to the playlist (relative or absolute paths, that's the responsibility - of the caller). By default the ``extm3u`` flag is set, to ensure a - save-operation writes an m3u-extended playlist (comment "#EXTM3U" at - the top of the file). - """ - self.media_list = media_list - self.extm3u = extm3u - - def write(self): - """Writes the m3u file to disk.""" - header = ["#EXTM3U"] if self.extm3u else [] - if not self.media_list: - raise EmptyPlaylistError - contents = header + self.media_list - with open(self.path, "w") as playlist_file: - playlist_file.writelines('\n'.join(contents)) - playlist_file.write('\n') # Final linefeed to prevent noeol file. - - def normpath(path): """Provide the canonical form of the path suitable for storing in the database. diff --git a/beets/util/m3u.py b/beets/util/m3u.py new file mode 100644 index 000000000..3db3f4084 --- /dev/null +++ b/beets/util/m3u.py @@ -0,0 +1,77 @@ +# This file is part of beets. +# Copyright 2022, J0J0 Todos. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +"""Provides utilities to read, write an manipulate m3u playlist files. +""" + + +class EmptyPlaylistError(Exception): + """An error that should be raised when a playlist file without media files + is saved or loaded. + """ + pass + + +class M3UFile(): + def __init__(self, path): + """Reads and writes m3u or m3u8 playlist files. + + ``path`` is the full path to the playlist file. + + The playlist file type, m3u or m3u8 is determined by 1) the ending + being m3u8 and 2) the file paths contained in the list being utf-8 + encoded. Since the list is passed from the outside, this is currently + out of control of this class. + """ + self.path = path + self.extm3u = False + self.media_list = [] + + def load(self): + """Reads the m3u file from disk and sets the object's attributes. + """ + with open(self.path, "r") as playlist_file: + raw_contents = playlist_file.readlines() + self.extm3u = True if raw_contents[0] == "#EXTM3U\n" else False + for line in raw_contents[1:]: + if line.startswith("#"): + # Some EXTM3U comment, do something. FIXME + continue + self.media_list.append(line) + if not self.media_list: + raise EmptyPlaylistError + + def set_contents(self, media_list, extm3u=True): + """Sets self.media_list to a list of media file paths, + + and sets additional flags, changing the final m3u-file's format. + + ``media_list`` is a list of paths to media files that should be added + to the playlist (relative or absolute paths, that's the responsibility + of the caller). By default the ``extm3u`` flag is set, to ensure a + save-operation writes an m3u-extended playlist (comment "#EXTM3U" at + the top of the file). + """ + self.media_list = media_list + self.extm3u = extm3u + + def write(self): + """Writes the m3u file to disk.""" + header = ["#EXTM3U"] if self.extm3u else [] + if not self.media_list: + raise EmptyPlaylistError + contents = header + self.media_list + with open(self.path, "w") as playlist_file: + playlist_file.writelines('\n'.join(contents)) + playlist_file.write('\n') # Final linefeed to prevent noeol file. diff --git a/beetsplug/convert.py b/beetsplug/convert.py index ab6e4c420..416fb9502 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -31,7 +31,7 @@ from beets import art from beets.util.artresizer import ArtResizer from beets.library import parse_query_string from beets.library import Item -from beets.util import M3UFile +from beets.util.m3u import M3UFile _fs_lock = threading.Lock() _temp_files = [] # Keep track of temporary transcoded files for deletion. From 8dc556d4204df2cb760e950a68dda050eaee179f Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Mon, 22 Aug 2022 11:23:33 +0200 Subject: [PATCH 38/68] convert: playlist: Use syspath() for file read and write operations. --- beets/util/m3u.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/beets/util/m3u.py b/beets/util/m3u.py index 3db3f4084..983833afb 100644 --- a/beets/util/m3u.py +++ b/beets/util/m3u.py @@ -16,6 +16,9 @@ """ +from beets.util import syspath + + class EmptyPlaylistError(Exception): """An error that should be raised when a playlist file without media files is saved or loaded. @@ -41,7 +44,7 @@ class M3UFile(): def load(self): """Reads the m3u file from disk and sets the object's attributes. """ - with open(self.path, "r") as playlist_file: + with open(syspath(self.path), "r") as playlist_file: raw_contents = playlist_file.readlines() self.extm3u = True if raw_contents[0] == "#EXTM3U\n" else False for line in raw_contents[1:]: @@ -72,6 +75,6 @@ class M3UFile(): if not self.media_list: raise EmptyPlaylistError contents = header + self.media_list - with open(self.path, "w") as playlist_file: + with open(syspath(self.path), "w") as playlist_file: playlist_file.writelines('\n'.join(contents)) playlist_file.write('\n') # Final linefeed to prevent noeol file. From cb630c45f6dadb43cbb4beef26434f71676b0b1b Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Mon, 22 Aug 2022 12:22:39 +0200 Subject: [PATCH 39/68] convert: playlist: Also use syspath() for contents of playlist. We want to have processed every entry in the media list we pass to the M3UFile instance. --- beetsplug/convert.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 416fb9502..88c99e53d 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -485,9 +485,9 @@ class ConvertPlugin(BeetsPlugin): # computers. self._log.info("Creating playlist file: {0}", playlist) items_paths = [ - item.destination( + util.syspath(item.destination( basedir=dest, path_formats=path_formats, fragment=True - ) for item in items + )) for item in items ] if not pretend: m3ufile = M3UFile(playlist) From c1908d551af57751ed906226ea299b2d1f4c118b Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Tue, 23 Aug 2022 08:00:18 +0200 Subject: [PATCH 40/68] convert: playlist: Document the feature leaving out the fact that #EXTM3U is added to the playlist file header (that important?). --- docs/plugins/convert.rst | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/docs/plugins/convert.rst b/docs/plugins/convert.rst index 622a8f2cd..8f00058b0 100644 --- a/docs/plugins/convert.rst +++ b/docs/plugins/convert.rst @@ -4,7 +4,8 @@ Convert Plugin The ``convert`` plugin lets you convert parts of your collection to a directory of your choice, transcoding audio and embedding album art along the way. It can transcode to and from any format using a configurable command -line. +line. Optionally an m3u playlist file containing all the converted files can be +saved to the destination path. Installation @@ -54,6 +55,21 @@ instead, passing ``-H`` (``--hardlink``) creates hard links. Note that album art embedding is disabled for files that are linked. Refer to the ``link`` and ``hardlink`` options below. +The ``-m`` (or ``--playlist``) option enables the plugin to create an m3u8 +playlist file in the destination folder given by the ``-d`` (``--dest``) option +or the ``dest`` configuration. Either a simple filename or a relative path plus +a filename can be passed. The generated playlist will always use relative paths +to the contained media files to ensure compatibility when read from external +drives or on computers other than the one used for the conversion. Also refer +to the ``playlist`` option below. + +Note that the classic m3u format doesn't support special characters in media +file paths, thus the m3u8 format which requires media file paths to be unicode, +is used. Typically a playlist file would be named *.m3u8. The name of the file +can be freely chosen by the user though. Since it is always ensured that paths +to media files are written as defined by the ``path`` configuration, a +generated playlist potentially could contain unicode characters no matter what +file ending was chosen. Configuration ------------- @@ -124,6 +140,12 @@ file. The available options are: Default: ``false``. - **delete_originals**: Transcoded files will be copied or moved to their destination, depending on the import configuration. By default, the original files are not modified by the plugin. This option deletes the original files after the transcoding step has completed. Default: ``false``. +- **playlist**: The name of a playlist file that should be written on each run + of the plugin. A relative file path (e.g `playlists/mylist.m3u8`) is allowed + as well. The final destination of the playlist file will always be relative + to the destination path (``dest``, ``--dest``, ``-d``). This configuration is + overridden by the ``-m`` (``--playlist``) command line option. + Default: none. You can also configure the format to use for transcoding (see the next section): From 2c1163cbc5c80e15c6f3813bff64b52a1fa5c393 Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Wed, 24 Aug 2022 23:14:04 +0200 Subject: [PATCH 41/68] convert: playlist: Linter and import fixes in m3u module and testsuite. --- beets/util/m3u.py | 19 +++++++------------ test/test_m3ufile.py | 20 ++++++++++++++------ 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/beets/util/m3u.py b/beets/util/m3u.py index 983833afb..94e366531 100644 --- a/beets/util/m3u.py +++ b/beets/util/m3u.py @@ -12,25 +12,21 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -"""Provides utilities to read, write an manipulate m3u playlist files. -""" +"""Provides utilities to read, write an manipulate m3u playlist files.""" from beets.util import syspath class EmptyPlaylistError(Exception): - """An error that should be raised when a playlist file without media files - is saved or loaded. - """ + """Raised when a playlist file without media files is saved or loaded.""" pass class M3UFile(): + """Reads and writes m3u or m3u8 playlist files.""" def __init__(self, path): - """Reads and writes m3u or m3u8 playlist files. - - ``path`` is the full path to the playlist file. + """``path`` is the absolute path to the playlist file. The playlist file type, m3u or m3u8 is determined by 1) the ending being m3u8 and 2) the file paths contained in the list being utf-8 @@ -42,8 +38,7 @@ class M3UFile(): self.media_list = [] def load(self): - """Reads the m3u file from disk and sets the object's attributes. - """ + """Reads the m3u file from disk and sets the object's attributes.""" with open(syspath(self.path), "r") as playlist_file: raw_contents = playlist_file.readlines() self.extm3u = True if raw_contents[0] == "#EXTM3U\n" else False @@ -56,9 +51,9 @@ class M3UFile(): raise EmptyPlaylistError def set_contents(self, media_list, extm3u=True): - """Sets self.media_list to a list of media file paths, + """Sets self.media_list to a list of media file paths. - and sets additional flags, changing the final m3u-file's format. + Also sets additional flags, changing the final m3u-file's format. ``media_list`` is a list of paths to media files that should be added to the playlist (relative or absolute paths, that's the responsibility diff --git a/test/test_m3ufile.py b/test/test_m3ufile.py index a3f8703b5..73dc57d54 100644 --- a/test/test_m3ufile.py +++ b/test/test_m3ufile.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2016, Johannes Tiefenbacher. +# Copyright 2016, J0J0 Todos. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -11,22 +11,23 @@ # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. +"""Testsuite for the M3UFile class.""" -from os import path, remove +from os import path from tempfile import mkdtemp from shutil import rmtree import unittest -# from unittest.mock import Mock, MagicMock - -from beets.util import M3UFile, EmptyPlaylistError -from beets.util import syspath, bytestring_path, py3_path, CHAR_REPLACE +from beets.util import bytestring_path +from beets.util.m3u import M3UFile, EmptyPlaylistError from test._common import RSRC class M3UFileTest(unittest.TestCase): + """Tests the M3UFile class.""" def test_playlist_write_empty(self): + """Test whether saving an empty playlist file raises an error.""" tempdir = bytestring_path(mkdtemp()) the_playlist_file = path.join(tempdir, b'playlist.m3u8') m3ufile = M3UFile(the_playlist_file) @@ -35,6 +36,7 @@ class M3UFileTest(unittest.TestCase): rmtree(tempdir) def test_playlist_write(self): + """Test saving ascii paths to a playlist file.""" tempdir = bytestring_path(mkdtemp()) the_playlist_file = path.join(tempdir, b'playlist.m3u') m3ufile = M3UFile(the_playlist_file) @@ -47,6 +49,7 @@ class M3UFileTest(unittest.TestCase): rmtree(tempdir) def test_playlist_write_unicode(self): + """Test saving unicode paths to a playlist file.""" tempdir = bytestring_path(mkdtemp()) the_playlist_file = path.join(tempdir, b'playlist.m3u8') m3ufile = M3UFile(the_playlist_file) @@ -59,6 +62,7 @@ class M3UFileTest(unittest.TestCase): rmtree(tempdir) def test_playlist_load_ascii(self): + """Test loading ascii paths from a playlist file.""" the_playlist_file = path.join(RSRC, b'playlist.m3u') m3ufile = M3UFile(the_playlist_file) m3ufile.load() @@ -66,6 +70,7 @@ class M3UFileTest(unittest.TestCase): '/This/is/a/path/to_a_file.mp3\n') def test_playlist_load_unicode(self): + """Test loading unicode paths from a playlist file.""" the_playlist_file = path.join(RSRC, b'playlist.m3u8') m3ufile = M3UFile(the_playlist_file) m3ufile.load() @@ -73,12 +78,14 @@ class M3UFileTest(unittest.TestCase): '/This/is/å/path/to_a_file.mp3\n') def test_playlist_load_extm3u(self): + """Test loading a playlist with an #EXTM3U header.""" the_playlist_file = path.join(RSRC, b'playlist.m3u') m3ufile = M3UFile(the_playlist_file) m3ufile.load() self.assertTrue(m3ufile.extm3u) def test_playlist_load_non_extm3u(self): + """Test loading a playlist without an #EXTM3U header.""" the_playlist_file = path.join(RSRC, b'playlist_non_ext.m3u') m3ufile = M3UFile(the_playlist_file) m3ufile.load() @@ -86,6 +93,7 @@ class M3UFileTest(unittest.TestCase): def suite(): + """This testsuite's main function.""" return unittest.TestLoader().loadTestsFromName(__name__) From a1baf9e94b404db5aa8e26604a9f929b9419fd99 Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Thu, 25 Aug 2022 08:24:37 +0200 Subject: [PATCH 42/68] convert: playlist: Fix rst linter error in docs Fix "inline empasis start string without endstring" error in docs. --- docs/plugins/convert.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/convert.rst b/docs/plugins/convert.rst index 8f00058b0..a68745da9 100644 --- a/docs/plugins/convert.rst +++ b/docs/plugins/convert.rst @@ -65,7 +65,7 @@ to the ``playlist`` option below. Note that the classic m3u format doesn't support special characters in media file paths, thus the m3u8 format which requires media file paths to be unicode, -is used. Typically a playlist file would be named *.m3u8. The name of the file +is used. Typically a playlist file would be named `*.m3u8`. The name of the file can be freely chosen by the user though. Since it is always ensured that paths to media files are written as defined by the ``path`` configuration, a generated playlist potentially could contain unicode characters no matter what From 785ef1576cfd42138080f35e0b906cfab0b258c5 Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Sat, 27 Aug 2022 15:22:50 +0200 Subject: [PATCH 43/68] convert: playlist: Use syspath() for media files loading as well. --- beets/util/m3u.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/util/m3u.py b/beets/util/m3u.py index 94e366531..eca958941 100644 --- a/beets/util/m3u.py +++ b/beets/util/m3u.py @@ -46,7 +46,7 @@ class M3UFile(): if line.startswith("#"): # Some EXTM3U comment, do something. FIXME continue - self.media_list.append(line) + self.media_list.append(syspath(line)) if not self.media_list: raise EmptyPlaylistError From da01be3d936638336ef9f43ae06dff8bd20b60f4 Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Sat, 27 Aug 2022 19:09:06 +0200 Subject: [PATCH 44/68] convert: playlist: Enforce utf-8 encoding on load() and write(). --- beets/util/m3u.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beets/util/m3u.py b/beets/util/m3u.py index eca958941..d5a55a65f 100644 --- a/beets/util/m3u.py +++ b/beets/util/m3u.py @@ -39,7 +39,7 @@ class M3UFile(): def load(self): """Reads the m3u file from disk and sets the object's attributes.""" - with open(syspath(self.path), "r") as playlist_file: + with open(syspath(self.path), "r", encoding="utf-8") as playlist_file: raw_contents = playlist_file.readlines() self.extm3u = True if raw_contents[0] == "#EXTM3U\n" else False for line in raw_contents[1:]: @@ -70,6 +70,6 @@ class M3UFile(): if not self.media_list: raise EmptyPlaylistError contents = header + self.media_list - with open(syspath(self.path), "w") as playlist_file: + with open(syspath(self.path), "w", encoding="utf-8") as playlist_file: playlist_file.writelines('\n'.join(contents)) playlist_file.write('\n') # Final linefeed to prevent noeol file. From 5f5be52a89de30442efede8fe3a948ba0c84bdc9 Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Sat, 27 Aug 2022 19:32:59 +0200 Subject: [PATCH 45/68] convert: playlist: Debug commit: Learn syspath() Learn what's happening in syspath(). --- beets/util/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 2319890a3..70ba7b996 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -409,11 +409,17 @@ def syspath(path, prefix=True): # reported as the FS encoding by Windows. Try both. try: path = path.decode('utf-8') + print("syspath: this is path:") + print(path) except UnicodeError: # The encoding should always be MBCS, Windows' broken # Unicode representation. encoding = sys.getfilesystemencoding() or sys.getdefaultencoding() path = path.decode(encoding, 'replace') + print("syspath: this is encoding:") + print(encoding) + print("syspath: this is path:") + print(path) # Add the magic prefix if it isn't already there. # https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247.aspx From bd5335f31fe021d3c04b9f681f6023816d0c1ccc Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Sun, 28 Aug 2022 13:19:33 +0200 Subject: [PATCH 46/68] convert: playlist: Separate unicode test for Windows --- test/rsrc/playlist_windows.m3u8 | 3 +++ test/test_m3ufile.py | 11 +++++++++++ 2 files changed, 14 insertions(+) create mode 100644 test/rsrc/playlist_windows.m3u8 diff --git a/test/rsrc/playlist_windows.m3u8 b/test/rsrc/playlist_windows.m3u8 new file mode 100644 index 000000000..97da8660a --- /dev/null +++ b/test/rsrc/playlist_windows.m3u8 @@ -0,0 +1,3 @@ +#EXTM3U +\\\\?\\/This/is/å/path/to_a_file.mp3 +\\\\?\\/This/is/another/path/tö_a_file.mp3 diff --git a/test/test_m3ufile.py b/test/test_m3ufile.py index 73dc57d54..4e13a9c1d 100644 --- a/test/test_m3ufile.py +++ b/test/test_m3ufile.py @@ -18,6 +18,7 @@ from os import path from tempfile import mkdtemp from shutil import rmtree import unittest +import sys from beets.util import bytestring_path from beets.util.m3u import M3UFile, EmptyPlaylistError @@ -69,6 +70,7 @@ class M3UFileTest(unittest.TestCase): self.assertEqual(m3ufile.media_list[0], '/This/is/a/path/to_a_file.mp3\n') + @unittest.skipIf(sys.platform == 'win32', 'win32') def test_playlist_load_unicode(self): """Test loading unicode paths from a playlist file.""" the_playlist_file = path.join(RSRC, b'playlist.m3u8') @@ -77,6 +79,15 @@ class M3UFileTest(unittest.TestCase): self.assertEqual(m3ufile.media_list[0], '/This/is/å/path/to_a_file.mp3\n') + @unittest.skipUnless(sys.platform == 'win32', 'win32') + def test_playlist_load_unicode_windows(self): + """Test loading unicode paths from a playlist file.""" + the_playlist_file = path.join(RSRC, b'playlist_windows.m3u8') + m3ufile = M3UFile(the_playlist_file) + m3ufile.load() + self.assertEqual(m3ufile.media_list[0], + '\\\\?\\/This/is/å/path/to_a_file.mp3\n') + def test_playlist_load_extm3u(self): """Test loading a playlist with an #EXTM3U header.""" the_playlist_file = path.join(RSRC, b'playlist.m3u') From b3d0c1cc1cacb79be56504edc3ade4596a8794e5 Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Sun, 28 Aug 2022 13:26:06 +0200 Subject: [PATCH 47/68] Revert "convert: playlist: Debug commit: Learn syspath()" This reverts commit 8a7519e5057e9c11a5f95c979b2fd5ac6c1fd9e2. --- beets/util/__init__.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 70ba7b996..2319890a3 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -409,17 +409,11 @@ def syspath(path, prefix=True): # reported as the FS encoding by Windows. Try both. try: path = path.decode('utf-8') - print("syspath: this is path:") - print(path) except UnicodeError: # The encoding should always be MBCS, Windows' broken # Unicode representation. encoding = sys.getfilesystemencoding() or sys.getdefaultencoding() path = path.decode(encoding, 'replace') - print("syspath: this is encoding:") - print(encoding) - print("syspath: this is path:") - print(path) # Add the magic prefix if it isn't already there. # https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247.aspx From 004d10a143fcb89291ead9c4293be828b7353d8e Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Sun, 28 Aug 2022 13:31:32 +0200 Subject: [PATCH 48/68] convert: playlist: Put actual Windows paths into fixture file for the Windows unittest. --- test/rsrc/playlist_windows.m3u8 | 4 ++-- test/test_m3ufile.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/rsrc/playlist_windows.m3u8 b/test/rsrc/playlist_windows.m3u8 index 97da8660a..c1f4af63a 100644 --- a/test/rsrc/playlist_windows.m3u8 +++ b/test/rsrc/playlist_windows.m3u8 @@ -1,3 +1,3 @@ #EXTM3U -\\\\?\\/This/is/å/path/to_a_file.mp3 -\\\\?\\/This/is/another/path/tö_a_file.mp3 +x:\This\is\å\path\to_a_file.mp3 +x:\This\is\another\path\tö_a_file.mp3 diff --git a/test/test_m3ufile.py b/test/test_m3ufile.py index 4e13a9c1d..f9fd37dbd 100644 --- a/test/test_m3ufile.py +++ b/test/test_m3ufile.py @@ -86,7 +86,7 @@ class M3UFileTest(unittest.TestCase): m3ufile = M3UFile(the_playlist_file) m3ufile.load() self.assertEqual(m3ufile.media_list[0], - '\\\\?\\/This/is/å/path/to_a_file.mp3\n') + 'x:\This\is\å\path\to_a_file.mp3\n') def test_playlist_load_extm3u(self): """Test loading a playlist with an #EXTM3U header.""" From 31b9e7afeb90978bdec72aeed8d0170fd3eec479 Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Sun, 28 Aug 2022 13:44:19 +0200 Subject: [PATCH 49/68] convert: playlist: Construct Windows path programatically --- test/test_m3ufile.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/test_m3ufile.py b/test/test_m3ufile.py index f9fd37dbd..e250e7b3a 100644 --- a/test/test_m3ufile.py +++ b/test/test_m3ufile.py @@ -85,8 +85,10 @@ class M3UFileTest(unittest.TestCase): the_playlist_file = path.join(RSRC, b'playlist_windows.m3u8') m3ufile = M3UFile(the_playlist_file) m3ufile.load() - self.assertEqual(m3ufile.media_list[0], - 'x:\This\is\å\path\to_a_file.mp3\n') + self.assertEqual( + m3ufile.media_list[0], + path.join('x:', 'This', 'is', 'å', 'path', 'to_a_file.mp3') + '\n' + ) def test_playlist_load_extm3u(self): """Test loading a playlist with an #EXTM3U header.""" From e4213714ba6b8e23d16d0289ed4bb6c96cf8b9ed Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Sun, 28 Aug 2022 13:46:47 +0200 Subject: [PATCH 50/68] convert: playlist: Disable prefix in syspath on in load method when loading media files to content list. --- beets/util/m3u.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/util/m3u.py b/beets/util/m3u.py index d5a55a65f..6a6aca4b5 100644 --- a/beets/util/m3u.py +++ b/beets/util/m3u.py @@ -46,7 +46,7 @@ class M3UFile(): if line.startswith("#"): # Some EXTM3U comment, do something. FIXME continue - self.media_list.append(syspath(line)) + self.media_list.append(syspath(line, prefix=False)) if not self.media_list: raise EmptyPlaylistError From 54d22bea6e0a5170cd8e8e6f016a7029a53fc0dd Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Sun, 28 Aug 2022 14:21:58 +0200 Subject: [PATCH 51/68] convert: playlist: Construct winpath before assert --- test/test_m3ufile.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_m3ufile.py b/test/test_m3ufile.py index e250e7b3a..a8cb09f66 100644 --- a/test/test_m3ufile.py +++ b/test/test_m3ufile.py @@ -83,11 +83,12 @@ class M3UFileTest(unittest.TestCase): def test_playlist_load_unicode_windows(self): """Test loading unicode paths from a playlist file.""" the_playlist_file = path.join(RSRC, b'playlist_windows.m3u8') + winpath = path.join('x:', 'This', 'is', 'å', 'path', 'to_a_file.mp3') m3ufile = M3UFile(the_playlist_file) m3ufile.load() self.assertEqual( m3ufile.media_list[0], - path.join('x:', 'This', 'is', 'å', 'path', 'to_a_file.mp3') + '\n' + winpath + '\n' ) def test_playlist_load_extm3u(self): From a641fd151e632d592712e79d3addd95bc87b5517 Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Sun, 28 Aug 2022 19:12:54 +0200 Subject: [PATCH 52/68] convert: playlist: debug winpath in test --- test/test_m3ufile.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/test_m3ufile.py b/test/test_m3ufile.py index a8cb09f66..976c40508 100644 --- a/test/test_m3ufile.py +++ b/test/test_m3ufile.py @@ -84,6 +84,8 @@ class M3UFileTest(unittest.TestCase): """Test loading unicode paths from a playlist file.""" the_playlist_file = path.join(RSRC, b'playlist_windows.m3u8') winpath = path.join('x:', 'This', 'is', 'å', 'path', 'to_a_file.mp3') + print("this is winpath:") + print(winpath) m3ufile = M3UFile(the_playlist_file) m3ufile.load() self.assertEqual( From 39efd23d06a6fa8868b242e305e9ecdbebc4fd08 Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Sun, 28 Aug 2022 19:37:10 +0200 Subject: [PATCH 53/68] convert: playlist: Fix winpath driveletter in test Needs to be put including (double) backslash! --- test/test_m3ufile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_m3ufile.py b/test/test_m3ufile.py index 976c40508..c5b7f49ec 100644 --- a/test/test_m3ufile.py +++ b/test/test_m3ufile.py @@ -83,7 +83,7 @@ class M3UFileTest(unittest.TestCase): def test_playlist_load_unicode_windows(self): """Test loading unicode paths from a playlist file.""" the_playlist_file = path.join(RSRC, b'playlist_windows.m3u8') - winpath = path.join('x:', 'This', 'is', 'å', 'path', 'to_a_file.mp3') + winpath = path.join('x:\\', 'This', 'is', 'å', 'path', 'to_a_file.mp3') print("this is winpath:") print(winpath) m3ufile = M3UFile(the_playlist_file) From ff03ecaa2774b5d8010749c88f2894181e0121c2 Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Sun, 28 Aug 2022 20:05:16 +0200 Subject: [PATCH 54/68] convert: playlist: Add another Windows test Add test_playlist_write_and_read_unicode_windows: Writes 2 media file paths containing unicode characters, reads them in using M3UFile class again and tests if the contents is correct. --- test/test_m3ufile.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/test_m3ufile.py b/test/test_m3ufile.py index c5b7f49ec..a59b8fb76 100644 --- a/test/test_m3ufile.py +++ b/test/test_m3ufile.py @@ -62,6 +62,32 @@ class M3UFileTest(unittest.TestCase): self.assertTrue(path.exists(the_playlist_file)) rmtree(tempdir) + @unittest.skipUnless(sys.platform == 'win32', 'win32') + def test_playlist_write_and_read_unicode_windows(self): + """Test saving unicode paths to a playlist file on Windows.""" + tempdir = bytestring_path(mkdtemp()) + the_playlist_file = path.join(tempdir, + b'playlist_write_and_read_windows.m3u8') + m3ufile = M3UFile(the_playlist_file) + m3ufile.set_contents([ + r"x:\This\is\å\path\to_a_file.mp3", + r"x:\This\is\another\path\tö_a_file.mp3" + ]) + m3ufile.write() + self.assertTrue(path.exists(the_playlist_file)) + m3ufile_read = M3UFile(the_playlist_file) + m3ufile_read.load() + self.assertEquals( + m3ufile.media_list[0], + path.join('x:\\', 'This', 'is', 'å', 'path', 'to_a_file.mp3') + ) + self.assertEquals( + m3ufile.media_list[1], + r"x:\This\is\another\path\tö_a_file.mp3", + path.join('x:\\', 'This', 'is', 'another', 'path', 'tö_a_file.mp3') + ) + rmtree(tempdir) + def test_playlist_load_ascii(self): """Test loading ascii paths from a playlist file.""" the_playlist_file = path.join(RSRC, b'playlist.m3u') From c28eb95ef2d4b3e2704f15a715cac128ddd061f6 Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Sun, 28 Aug 2022 20:20:51 +0200 Subject: [PATCH 55/68] convert: playlist: Remove debug print winpath in test. --- test/test_m3ufile.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/test_m3ufile.py b/test/test_m3ufile.py index a59b8fb76..fedfa90a6 100644 --- a/test/test_m3ufile.py +++ b/test/test_m3ufile.py @@ -110,8 +110,6 @@ class M3UFileTest(unittest.TestCase): """Test loading unicode paths from a playlist file.""" the_playlist_file = path.join(RSRC, b'playlist_windows.m3u8') winpath = path.join('x:\\', 'This', 'is', 'å', 'path', 'to_a_file.mp3') - print("this is winpath:") - print(winpath) m3ufile = M3UFile(the_playlist_file) m3ufile.load() self.assertEqual( From d248063f96f296dfc24cc48af318fbc515e6a9d5 Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Mon, 29 Aug 2022 14:15:03 +0200 Subject: [PATCH 56/68] convert: playlist: Improve --playlist help text --- beetsplug/convert.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 88c99e53d..9493a5fc2 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -180,14 +180,14 @@ class ConvertPlugin(BeetsPlugin): help='hardlink files that do not \ need transcoding. Overrides --link.') cmd.parser.add_option('-m', '--playlist', action='store', - help='''the name of an m3u8 playlist file to - be created in the root of the destination folder. - The m3u8 format ensures special characters - support by using unicode to save media file - paths. Relative paths are used to point to media - files ensuring a working playlist when - transferred to a different computer (eg. when - opened from an external drive).''') + help='''create an m3u8 playlist file containing + the converted files. The playlist file will be + saved below the destination directory, thus + PLAYLIST could be a file name or a relative path. + To ensure a working playlist when transferred to + a different computer, or opened from an external + drive, relative paths pointing to media files + will be used.''') cmd.parser.add_album_option() cmd.func = self.convert_func return [cmd] From 068208f71e8f1058b7f9ab7afb47dbd9c4a9cc0b Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Wed, 21 Sep 2022 08:31:42 +0200 Subject: [PATCH 57/68] convert: Fix copyright year in test_m3ufile.py --- test/test_m3ufile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_m3ufile.py b/test/test_m3ufile.py index fedfa90a6..07635597b 100644 --- a/test/test_m3ufile.py +++ b/test/test_m3ufile.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2016, J0J0 Todos. +# Copyright 2022, J0J0 Todos. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the From 20a0012f796cf2a07ce4983a19379a7b03abb53b Mon Sep 17 00:00:00 2001 From: J0J0 Todos Date: Sat, 4 Mar 2023 20:38:56 +0100 Subject: [PATCH 58/68] convert: playlist: Use normpath for playlist file Fixes FileNotFoundError when for example a tilde (~) characteris used for a --dest path. --- beets/util/m3u.py | 4 ++-- beetsplug/convert.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/beets/util/m3u.py b/beets/util/m3u.py index 6a6aca4b5..75468c0fa 100644 --- a/beets/util/m3u.py +++ b/beets/util/m3u.py @@ -15,7 +15,7 @@ """Provides utilities to read, write an manipulate m3u playlist files.""" -from beets.util import syspath +from beets.util import syspath, normpath class EmptyPlaylistError(Exception): @@ -70,6 +70,6 @@ class M3UFile(): if not self.media_list: raise EmptyPlaylistError contents = header + self.media_list - with open(syspath(self.path), "w", encoding="utf-8") as playlist_file: + with open(syspath(normpath(self.path)), "w", encoding="utf-8") as playlist_file: playlist_file.writelines('\n'.join(contents)) playlist_file.write('\n') # Final linefeed to prevent noeol file. diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 9493a5fc2..b77b9dfa2 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -483,7 +483,8 @@ class ConvertPlugin(BeetsPlugin): # media file paths to be unicode. Additionally we use relative # paths to ensure readability of the playlist on remote # computers. - self._log.info("Creating playlist file: {0}", playlist) + self._log.info("Creating playlist file: {0}", + util.normpath(playlist)) items_paths = [ util.syspath(item.destination( basedir=dest, path_formats=path_formats, fragment=True From 46fb8fef914c84a9e1279a09ff131fefd05f0e82 Mon Sep 17 00:00:00 2001 From: J0J0 Todos Date: Sat, 4 Mar 2023 20:45:27 +0100 Subject: [PATCH 59/68] convert: playlist: Fix typo in m3u module docstring --- beets/util/m3u.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/util/m3u.py b/beets/util/m3u.py index 75468c0fa..7903157a5 100644 --- a/beets/util/m3u.py +++ b/beets/util/m3u.py @@ -12,7 +12,7 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -"""Provides utilities to read, write an manipulate m3u playlist files.""" +"""Provides utilities to read, write and manipulate m3u playlist files.""" from beets.util import syspath, normpath From 952aa0baddb92dcacce782f64423d9e27cd16d7b Mon Sep 17 00:00:00 2001 From: J0J0 Todos Date: Sun, 5 Mar 2023 12:39:41 +0100 Subject: [PATCH 60/68] convert: playlist: Handle playlist path subdirs The M3UFile.write() method now creates potential parent directories in a passed playlist path. util.mkdirall() handles errors nicely already and would exit the mainprogram before potential subsequent failures could happen (it raises util.FilesystemError). --- beets/util/m3u.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/beets/util/m3u.py b/beets/util/m3u.py index 7903157a5..e026ccf03 100644 --- a/beets/util/m3u.py +++ b/beets/util/m3u.py @@ -15,7 +15,7 @@ """Provides utilities to read, write and manipulate m3u playlist files.""" -from beets.util import syspath, normpath +from beets.util import syspath, normpath, mkdirall class EmptyPlaylistError(Exception): @@ -65,11 +65,17 @@ class M3UFile(): self.extm3u = extm3u def write(self): - """Writes the m3u file to disk.""" + """Writes the m3u file to disk. + + Handles the creation of potential parent directories. + """ header = ["#EXTM3U"] if self.extm3u else [] if not self.media_list: raise EmptyPlaylistError contents = header + self.media_list - with open(syspath(normpath(self.path)), "w", encoding="utf-8") as playlist_file: + pl_normpath = normpath(self.path) + mkdirall(pl_normpath) + + with open(syspath(pl_normpath), "w", encoding="utf-8") as playlist_file: playlist_file.writelines('\n'.join(contents)) playlist_file.write('\n') # Final linefeed to prevent noeol file. From 0884e67d3544cbef150ea84fa45b77a6bb657bb1 Mon Sep 17 00:00:00 2001 From: J0J0 Todos Date: Sun, 5 Mar 2023 13:31:26 +0100 Subject: [PATCH 61/68] convert: playlist: Handle errors on read/write operations of a playlist in M3UFile class, by catching any "OSError" and raising util.FilesystemError. --- beets/util/m3u.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/beets/util/m3u.py b/beets/util/m3u.py index e026ccf03..0d36e5aa1 100644 --- a/beets/util/m3u.py +++ b/beets/util/m3u.py @@ -14,8 +14,9 @@ """Provides utilities to read, write and manipulate m3u playlist files.""" +import traceback -from beets.util import syspath, normpath, mkdirall +from beets.util import syspath, normpath, mkdirall, FilesystemError class EmptyPlaylistError(Exception): @@ -39,8 +40,14 @@ class M3UFile(): def load(self): """Reads the m3u file from disk and sets the object's attributes.""" - with open(syspath(self.path), "r", encoding="utf-8") as playlist_file: - raw_contents = playlist_file.readlines() + pl_normpath = normpath(self.path) + try: + with open(syspath(pl_normpath), "r", encoding="utf-8") as pl_file: + raw_contents = pl_file.readlines() + except OSError as exc: + raise FilesystemError(exc, 'read', (pl_normpath, ), + traceback.format_exc()) + self.extm3u = True if raw_contents[0] == "#EXTM3U\n" else False for line in raw_contents[1:]: if line.startswith("#"): @@ -76,6 +83,10 @@ class M3UFile(): pl_normpath = normpath(self.path) mkdirall(pl_normpath) - with open(syspath(pl_normpath), "w", encoding="utf-8") as playlist_file: - playlist_file.writelines('\n'.join(contents)) - playlist_file.write('\n') # Final linefeed to prevent noeol file. + try: + with open(syspath(pl_normpath), "w", encoding="utf-8") as pl_file: + pl_file.writelines('\n'.join(contents)) + pl_file.write('\n') # Final linefeed to prevent noeol file. + except OSError as exc: + raise FilesystemError(exc, 'create', (pl_normpath, ), + traceback.format_exc()) From a4d03ef5867901a03dfb1c1accf9f2f35fd7b2da Mon Sep 17 00:00:00 2001 From: J0J0 Todos Date: Wed, 22 Mar 2023 19:55:09 +0100 Subject: [PATCH 62/68] convert: playlist: M3U write + contents as bytes Make sure we stay with the beets standard of handling everything internally as bytes. - M3UFile.write() method writes in wb mode. - Playlist contents and EXTM3U header is handled as bytes. - item.destination() gives us unicode string paths, we tranlate to bytes using util.bytestring_path(). - Fix test_playlist*write* tests to encode UTF-8 assert strings as bytes using bytestring_path() before comparision. --- beets/util/m3u.py | 9 +++++---- beetsplug/convert.py | 2 +- test/test_m3ufile.py | 20 +++++++++++--------- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/beets/util/m3u.py b/beets/util/m3u.py index 0d36e5aa1..0f03d78da 100644 --- a/beets/util/m3u.py +++ b/beets/util/m3u.py @@ -76,7 +76,7 @@ class M3UFile(): Handles the creation of potential parent directories. """ - header = ["#EXTM3U"] if self.extm3u else [] + header = [b"#EXTM3U"] if self.extm3u else [] if not self.media_list: raise EmptyPlaylistError contents = header + self.media_list @@ -84,9 +84,10 @@ class M3UFile(): mkdirall(pl_normpath) try: - with open(syspath(pl_normpath), "w", encoding="utf-8") as pl_file: - pl_file.writelines('\n'.join(contents)) - pl_file.write('\n') # Final linefeed to prevent noeol file. + with open(syspath(pl_normpath), "wb") as pl_file: + for line in contents: + pl_file.write(line + b'\n') + pl_file.write(b'\n') # Final linefeed to prevent noeol file. except OSError as exc: raise FilesystemError(exc, 'create', (pl_normpath, ), traceback.format_exc()) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index b77b9dfa2..ee9cf3641 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -486,7 +486,7 @@ class ConvertPlugin(BeetsPlugin): self._log.info("Creating playlist file: {0}", util.normpath(playlist)) items_paths = [ - util.syspath(item.destination( + util.bytestring_path(item.destination( basedir=dest, path_formats=path_formats, fragment=True )) for item in items ] diff --git a/test/test_m3ufile.py b/test/test_m3ufile.py index 07635597b..b133f65e1 100644 --- a/test/test_m3ufile.py +++ b/test/test_m3ufile.py @@ -42,8 +42,8 @@ class M3UFileTest(unittest.TestCase): the_playlist_file = path.join(tempdir, b'playlist.m3u') m3ufile = M3UFile(the_playlist_file) m3ufile.set_contents([ - '/This/is/a/path/to_a_file.mp3', - '/This/is/another/path/to_a_file.mp3', + bytestring_path('/This/is/a/path/to_a_file.mp3'), + bytestring_path('/This/is/another/path/to_a_file.mp3') ]) m3ufile.write() self.assertTrue(path.exists(the_playlist_file)) @@ -55,8 +55,8 @@ class M3UFileTest(unittest.TestCase): the_playlist_file = path.join(tempdir, b'playlist.m3u8') m3ufile = M3UFile(the_playlist_file) m3ufile.set_contents([ - '/This/is/å/path/to_a_file.mp3', - '/This/is/another/path/tö_a_file.mp3', + bytestring_path('/This/is/å/path/to_a_file.mp3'), + bytestring_path('/This/is/another/path/tö_a_file.mp3') ]) m3ufile.write() self.assertTrue(path.exists(the_playlist_file)) @@ -70,8 +70,8 @@ class M3UFileTest(unittest.TestCase): b'playlist_write_and_read_windows.m3u8') m3ufile = M3UFile(the_playlist_file) m3ufile.set_contents([ - r"x:\This\is\å\path\to_a_file.mp3", - r"x:\This\is\another\path\tö_a_file.mp3" + bytestring_path(r"x:\This\is\å\path\to_a_file.mp3"), + bytestring_path(r"x:\This\is\another\path\tö_a_file.mp3") ]) m3ufile.write() self.assertTrue(path.exists(the_playlist_file)) @@ -79,12 +79,14 @@ class M3UFileTest(unittest.TestCase): m3ufile_read.load() self.assertEquals( m3ufile.media_list[0], - path.join('x:\\', 'This', 'is', 'å', 'path', 'to_a_file.mp3') + bytestring_path( + path.join('x:\\', 'This', 'is', 'å', 'path', 'to_a_file.mp3')) ) self.assertEquals( m3ufile.media_list[1], - r"x:\This\is\another\path\tö_a_file.mp3", - path.join('x:\\', 'This', 'is', 'another', 'path', 'tö_a_file.mp3') + bytestring_path(r"x:\This\is\another\path\tö_a_file.mp3"), + bytestring_path(path.join( + 'x:\\', 'This', 'is', 'another', 'path', 'tö_a_file.mp3')) ) rmtree(tempdir) From 99231160a76bca161b14abbd10a32716436fcfbe Mon Sep 17 00:00:00 2001 From: J0J0 Todos Date: Fri, 24 Mar 2023 08:38:59 +0100 Subject: [PATCH 63/68] convert: playlist: M3U read as bytes - M3UFile.read() method reads in rb mode. - M3UFile.read() method handles removal of (platform specific) line endings. - Playlist contents and EXTM3U header is handled as bytes. - Fix test_playlist*read* tests to encode playlist UTF-8 assert strings to bytes using bytestring_path() before comparision. - Fixture playlist_windows.m3u8 is now actually Windows formatted (\r\n + BOM) --- beets/util/m3u.py | 10 +++++----- test/rsrc/playlist_windows.m3u8 | 6 +++--- test/test_m3ufile.py | 10 ++++++---- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/beets/util/m3u.py b/beets/util/m3u.py index 0f03d78da..9c961a291 100644 --- a/beets/util/m3u.py +++ b/beets/util/m3u.py @@ -42,18 +42,18 @@ class M3UFile(): """Reads the m3u file from disk and sets the object's attributes.""" pl_normpath = normpath(self.path) try: - with open(syspath(pl_normpath), "r", encoding="utf-8") as pl_file: + with open(syspath(pl_normpath), "rb") as pl_file: raw_contents = pl_file.readlines() except OSError as exc: raise FilesystemError(exc, 'read', (pl_normpath, ), traceback.format_exc()) - self.extm3u = True if raw_contents[0] == "#EXTM3U\n" else False + self.extm3u = True if raw_contents[0].rstrip() == b"#EXTM3U" else False for line in raw_contents[1:]: - if line.startswith("#"): - # Some EXTM3U comment, do something. FIXME + if line.startswith(b"#"): + # Support for specific EXTM3U comments could be added here. continue - self.media_list.append(syspath(line, prefix=False)) + self.media_list.append(normpath(line.rstrip())) if not self.media_list: raise EmptyPlaylistError diff --git a/test/rsrc/playlist_windows.m3u8 b/test/rsrc/playlist_windows.m3u8 index c1f4af63a..f75dfd74f 100644 --- a/test/rsrc/playlist_windows.m3u8 +++ b/test/rsrc/playlist_windows.m3u8 @@ -1,3 +1,3 @@ -#EXTM3U -x:\This\is\å\path\to_a_file.mp3 -x:\This\is\another\path\tö_a_file.mp3 +#EXTM3U +x:\This\is\å\path\to_a_file.mp3 +x:\This\is\another\path\tö_a_file.mp3 diff --git a/test/test_m3ufile.py b/test/test_m3ufile.py index b133f65e1..a24dc6ca8 100644 --- a/test/test_m3ufile.py +++ b/test/test_m3ufile.py @@ -90,13 +90,14 @@ class M3UFileTest(unittest.TestCase): ) rmtree(tempdir) + @unittest.skipIf(sys.platform == 'win32', 'win32') def test_playlist_load_ascii(self): """Test loading ascii paths from a playlist file.""" the_playlist_file = path.join(RSRC, b'playlist.m3u') m3ufile = M3UFile(the_playlist_file) m3ufile.load() self.assertEqual(m3ufile.media_list[0], - '/This/is/a/path/to_a_file.mp3\n') + bytestring_path('/This/is/a/path/to_a_file.mp3')) @unittest.skipIf(sys.platform == 'win32', 'win32') def test_playlist_load_unicode(self): @@ -105,18 +106,19 @@ class M3UFileTest(unittest.TestCase): m3ufile = M3UFile(the_playlist_file) m3ufile.load() self.assertEqual(m3ufile.media_list[0], - '/This/is/å/path/to_a_file.mp3\n') + bytestring_path('/This/is/å/path/to_a_file.mp3')) @unittest.skipUnless(sys.platform == 'win32', 'win32') def test_playlist_load_unicode_windows(self): """Test loading unicode paths from a playlist file.""" the_playlist_file = path.join(RSRC, b'playlist_windows.m3u8') - winpath = path.join('x:\\', 'This', 'is', 'å', 'path', 'to_a_file.mp3') + winpath = bytestring_path(path.join( + 'x:\\', 'This', 'is', 'å', 'path', 'to_a_file.mp3')) m3ufile = M3UFile(the_playlist_file) m3ufile.load() self.assertEqual( m3ufile.media_list[0], - winpath + '\n' + winpath ) def test_playlist_load_extm3u(self): From 6777b49bcff3f12013dc78ec0845483ff0c787f9 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 1 Apr 2023 15:46:02 -0700 Subject: [PATCH 64/68] Appease flake8 --- beets/importer.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/beets/importer.py b/beets/importer.py index 4b448c557..96d1f17df 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -1123,16 +1123,14 @@ class ArchiveImportTask(SentinelImportTask): try: archive.extractall(extract_to) - # Adjust the files' mtimes to match the information from the archive. Inspired by: - # https://stackoverflow.com/q/9813243 - + # Adjust the files' mtimes to match the information from the + # archive. Inspired by: https://stackoverflow.com/q/9813243 for f in archive.infolist(): # The date_time will need to adjusted otherwise # the item will have the current date_time of extraction. # The (0, 0, -1) is added to date_time because the # function time.mktime expects a 9-element tuple. # The -1 indicates that the DST flag is unknown. - date_time = time.mktime(f.date_time + (0, 0, -1)) fullpath = os.path.join(extract_to, f.filename) os.utime(fullpath, (date_time, date_time)) From 16e361baf3f9f68dd396c604c427830c93086efb Mon Sep 17 00:00:00 2001 From: J0J0 Todos Date: Sun, 2 Apr 2023 13:08:06 +0200 Subject: [PATCH 65/68] convert: playlist: item_paths relative to playlist Ensure entries in items_paths are generated with a path relative to the location of the playlist file. --- beetsplug/convert.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index ee9cf3641..374b68e74 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -483,12 +483,13 @@ class ConvertPlugin(BeetsPlugin): # media file paths to be unicode. Additionally we use relative # paths to ensure readability of the playlist on remote # computers. - self._log.info("Creating playlist file: {0}", - util.normpath(playlist)) + pl_normpath = util.normpath(playlist) + pl_dir = os.path.dirname(pl_normpath) + self._log.info("Creating playlist file {0}", pl_normpath) items_paths = [ - util.bytestring_path(item.destination( - basedir=dest, path_formats=path_formats, fragment=True - )) for item in items + os.path.relpath(util.bytestring_path(item.destination( + basedir=dest, path_formats=path_formats, fragment=False + )), pl_dir) for item in items ] if not pretend: m3ufile = M3UFile(playlist) From b3b26efe88bf3dc41bacd4fbc27249a3cd6352e7 Mon Sep 17 00:00:00 2001 From: elyang0214 Date: Sun, 2 Apr 2023 11:13:53 -0400 Subject: [PATCH 66/68] Updated documentation to be less redundant in docs/referece/cli.rst and more accurate in docs/changelog.rst. --- docs/changelog.rst | 2 +- docs/reference/cli.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index d2f9e4c6e..6aa2c7302 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,7 +11,7 @@ for Python 3.6). New features: -* Added UTF-8 encoding enforcement to imported files in `beets/beets/ui/commands.py`. +* --from-logfile now parses log files using a UTF-8 encoding in `beets/beets/ui/commands.py`. :bug:`4693` * Added additional error handling for `spotify` plugin. :bug:`4686` diff --git a/docs/reference/cli.rst b/docs/reference/cli.rst index 7737c4498..da119d0f8 100644 --- a/docs/reference/cli.rst +++ b/docs/reference/cli.rst @@ -172,7 +172,7 @@ Optional command flags: Just point the ``beet import`` command at a directory of files that are already catalogged in your library. Beets will automatically detect this - situation and avoid duplicating any items. A UTF-8 encoding will be enforced on your imported file. In this situation, the "copy + situation and avoid duplicating any items. In this situation, the "copy files" option (``-c``/``-C`` on the command line or ``copy`` in the config file) has slightly different behavior: it causes files to be *moved*, rather than duplicated, if they're already in your library. (The same is From 86929eb6a01407e48bf511a0bc63d32508f10b59 Mon Sep 17 00:00:00 2001 From: J0J0 Todos Date: Sun, 2 Apr 2023 13:22:09 +0200 Subject: [PATCH 67/68] convert: playlist: Adapt code comments - Remove initial comment around playlist entry condition (which is better suited for user docs anyway, and stated there already) - Add explanation above the items_paths playlist contents creation list comprehension. --- beetsplug/convert.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 374b68e74..ef6865597 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -476,16 +476,13 @@ class ConvertPlugin(BeetsPlugin): link, hardlink, threads, items) if playlist: - # When playlist arg is passed create an m3u8 file in dest folder. - # - # The classic m3u format doesn't support special characters in - # media file paths, thus we use the m3u8 format which requires - # media file paths to be unicode. Additionally we use relative - # paths to ensure readability of the playlist on remote - # computers. + # Playlist paths are understood as relative to the dest directory. pl_normpath = util.normpath(playlist) pl_dir = os.path.dirname(pl_normpath) self._log.info("Creating playlist file {0}", pl_normpath) + # Generates a list of paths to media files, ensures the paths are + # relative to the playlist's location and translates the unicode + # strings we get from item.destination to bytes. items_paths = [ os.path.relpath(util.bytestring_path(item.destination( basedir=dest, path_formats=path_formats, fragment=False From 94784c2c2939d85837349a8a27c2c5b146cb1a65 Mon Sep 17 00:00:00 2001 From: J0J0 Todos Date: Sun, 2 Apr 2023 21:14:46 +0200 Subject: [PATCH 68/68] convert: playlist: Documentation overhaul --- docs/plugins/convert.rst | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/docs/plugins/convert.rst b/docs/plugins/convert.rst index a68745da9..d70d354bf 100644 --- a/docs/plugins/convert.rst +++ b/docs/plugins/convert.rst @@ -57,19 +57,16 @@ Refer to the ``link`` and ``hardlink`` options below. The ``-m`` (or ``--playlist``) option enables the plugin to create an m3u8 playlist file in the destination folder given by the ``-d`` (``--dest``) option -or the ``dest`` configuration. Either a simple filename or a relative path plus -a filename can be passed. The generated playlist will always use relative paths -to the contained media files to ensure compatibility when read from external -drives or on computers other than the one used for the conversion. Also refer -to the ``playlist`` option below. +or the ``dest`` configuration. The path to the playlist file can either be +absolute or relative to the ``dest`` directory. The contents will always be +relative paths to media files, which tries to ensure compatibility when read +from external drives or on computers other than the one used for the +conversion. There is one caveat though: A list generated on Unix/macOS can't be +read on Windows and vice versa. -Note that the classic m3u format doesn't support special characters in media -file paths, thus the m3u8 format which requires media file paths to be unicode, -is used. Typically a playlist file would be named `*.m3u8`. The name of the file -can be freely chosen by the user though. Since it is always ensured that paths -to media files are written as defined by the ``path`` configuration, a -generated playlist potentially could contain unicode characters no matter what -file ending was chosen. +Depending on the beets user's settings a generated playlist potentially could +contain unicode characters. This is supported, playlists are written in [m3u8 +format](https://en.wikipedia.org/wiki/M3U#M3U8). Configuration -------------