Git Product home page Git Product logo

beets-filetote's Introduction

Filetote plugin for beets

MIT license CI GitHub release PyPI PyPI - Python Version

A plugin that moves non-music extra files, attachments, and artifacts during imports and CLI file manipulation actions (move, modify, reimport, etc.) for beets, a music library manager (and much more!).

This plugin is supported/runs in beets v1.6.0. The unrealease "latest" version of beets is not (yet) officially supported.

Installing

Stable

The stable version of the plugin is available from PyPI and can be installed using pip3:

pip3 install beets-filetote

Important Note: Python versions <3.7 will need to also install the dataclasses module.

Configuration

You will need to enable the plugin in beets' config.yaml:

plugins: filetote

It can copy files by file extension:

filetote:
  extensions: .cue .log

Or copy all non-music files:

filetote:
  extensions: .*

Or copy files by filename:

filetote:
  filenames: song.log

Or match based on a "pattern" (glob pattern):

filetote:
  patterns:
    artworkdir:
      - "[aA]rtwork/"

It can look for and target "pairs" (files having the same name as a matching or "paired" media item/track):

filetote:
  pairing:
    enabled: true

You can specify pairing to happen to certain extensions, and even target/include only paired files:

filetote:
  pairing:
    enabled: true
    pairing_only: true
    extensions: ".lrc"

It can also exclude files by name:

filetote:
  exclude: song_lyrics.nfo

And print what got left:

filetote:
  print_ignored: true

exclude-d files take precedence over other matching, meaning exclude will override other matches by either extensions or filenames.

File Handling & Renaming

In order to collect extra files and artifacts, Filetote needs to be told which types of files it should care about. This can be done using the following:

  • Extensions (ext:): Specify individual extensions like .cue or .log, or catch all non-music files with .*.
  • Filenames (filename:): Match specific filenames like cover.jpg or organize artwork with [aA]rtwork/*.
  • Patterns (pattern:): Use flexible glob patterns for more control, like matching all logs in a subfolder: CD1/*.log.
  • Pairing: Move files with the same name as imported music items, like .lrc lyrics or album logs.

Filetote Renaming Basics

Unless otherwise specified, the default name for artifacts and extra files is: $albumpath/$old_filename. This means that by default, the file is essentially moved/copied into destination directory of the music item it gets grabbed with. This also means that the album folder is flattened and any subdirectory is removed by default. To preserve subdirectories, see $subpath usage here.

Configuration for renaming works in much the same way as beets Path Formats, including the standard metadata values provided by beets. Filetote provides the below new path queries, which each takes a single corresponding value. These can be defined in either the top-level paths section of Beet's config or in the paths section of Filetote's config. Both of the following are equivalent:

paths:
  ext:.log: $albumpath/$artist - $album
filetote:
  paths:
    ext:.log: $albumpath/$artist - $album
New path queries

These are the new path queries added by Filetote, from most to least specific:

  • filename:
  • paired_ext:
  • pattern:
  • ext:

This means that the filename: path query will take precedence over paired_ext:, pattern:, and ext: if a given file qualifies for them. This also means that the value in paired_ext: will take precedence over pattern: and ext:, and pattern: is higher priority than ext:.

Renaming considerations

Renaming has the following considerations:

The fields available include the standard metadata values of the imported item ($albumartist, $album, $title, etc.), along with Filetote-specific values of:

  • $albumpath: the entire path of the new destination of the item/track (a useful shorthand for when the extra/artifact file will be moved allongside the item/track).
    • Note: Beets doesn't have a strict "album" path concept. All references are relative to Items (the actual media files). This is especially relevant for multi-disc files/albums, but usually isn't a problem. Check the section on multi-discs here for more details.
  • $subpath: Represents any subdirectories under the base album path where an extra/artifact file resides. For use when it is desirable to preserve the directory hierarchy in the albums. This respects the original capitalization of directory names. Defaults to an empty string when no subdirectories exist.
    • Example: If an extra file is located in a subdirectory named "Extras" under the album path, $subpath would be set to "Extras/" (with the same casing).
  • $old_filename: the filename of the extra/artifact file before its renamed.
  • $medianame_old: the filename of the item/track triggering it, before it's renamed.
  • $medianame_new: the filename of the item/track triggering it, after it's renamed.

The full set of built in functions are also supported, with the exception of %aunique - which will return an empty string.

Note that the fields mentioned above are not usable within other plugins like inline. But inline and other plugins should be fine otherwise.

Important Note: if the rename is set and there are multiple files that qualify, only the first will be added to the library (new folder); other files that subsequently match will not be saved/renamed. To work around this, $old_filename can be used to help with adding uniqueness to the name.

Subpath Renaming Example

The following configuration or template string will be applied to .log files by using the $subpath and will rename log file to: ~/Music/Artist/2014 - Album/Extras/Artist - Album.log

This assumes that the original file is in the subdirectory (subpath) of Extras/. Any other .log files in other subdirectories or in the root of the album will be moved accordingly. If a more targeted approach is needed, this can be combined with the pattern: query.

Note: $subpath automatically adds in path separators including the end one if there are subdirectories.

paths:
  ext:.log: $albumpath/$subpath$artist - $album

Extension (ext:)

Filename can match on the extension of the file, in a space-delimited list (i.e., a string sequence). Use .* to match all file extensions.

Extension Example Configuration

This example will match any file which has an extension of either .lrc or .log, across all subfolders.

filetote:
  ext: .lrc .log
Extension Renaming Example

The following configuration or template string will be applied to .log files by using the ext: query and will rename log file to: ~/Music/Artist/2014 - Album/Artist - Album.log

paths:
  ext:.log: $albumpath/$artist - $album

Filename (filename:)

Filetote can match on the actual name (including extension) of the file, in a space-delimited list (string sequence). filename: will match across any subdirectories, meaning targeting a filename in a specific subdirectory will not work (this functionality can be achieved using a pattern, however).

Filename Example Configuration

This example will match if the filename of the given artifact or extra file matches the name exactly as specified, either cover.jpg or artifact.nfo.

filetote:
  filenames: cover.jpg artifact.nfo
Filename Renaming Example

The following configuration will rename the specific artifact.nfo file to: ~/Music/Artist/2014 - Album/Artist - Album.nfo

filetote:
  paths:
    filename:artifact.nfo: $albumpath/$artist - $album
  filenames: cover.jpg artifact.nfo

Pattern (pattern:)

Filetote can match on a given pattern as specified using glob patterns. This allows for more specific matching, like grabbing only PNG artwork files. Paths in the pattern are relative to the root of the importing album. Hence, if there are subdirectories in the album's folder (for multidisc setups, for instance, e.g., albumpath/CD1), the album's path would be the base/root for the pattern (ex: CD1/*.jpg). Patterns will work with or without the proceeding slash (/). Note: Windows users will need to use the appropriate slash (\).

Patterns specifying folders with a trailing slash will (ex: albumpath/) will match every file in that subdirectory irrespective of name or extension (it is equivalent to albumpath/*.*).

Patterns are defined by a name so that any customization for renaming can apply to the pattern when specifying the path (ex: pattern:artworkdir; see the section on renaming below).

Pattern Example Configuration

This example will match if the filename of the given artifact or extra file matches the name exactly as specified, either cover.jpg or artifact.nfo.

This example will match all files within the given subdirectory of either artwork/ or Artwork/. Since it's not otherwise specified, [aA]rtwork/ will grab all non-media files in that subdirectory irrespective of name or extension.

filetote:
  patterns:
    artworkdir:
      - "[aA]rtwork/"
Pattern Renaming Example

The following pattern configuration will rename the file artwork/cover.jpeg to: ~/Music/Artist/2014 - Album/artwork/cover.jpeg

filetote:
  paths:
    pattern:artworkdir: $albumpath/artwork/$old_filename
  patterns:
    artworkdir:
      - "[aA]rtwork/"

Pairing

Filetote can specially target related files like lyrics or logs with the same name as music files ("paired" files). This keeps related files together, making your library even more organized. When enabled, it will match and move those files having the same name as a matching music file. Pairing can be configured to target only certain extensions, such as .lrc.

Note: Pairing takes precedence over other Filetote rules like filename or patterns.

Pairing Example Configuration

This example configuration will grab paired .lrc files, along with any artwork files:

filetote:
  pairing:
    enabled: true
    extensions: ".lrc"
  patterns:
    artworkdir:
          - "[aA]rtwork/"

Filetote can also be configured to only target paired files, which will ignore other Filetote configurations such as filename or patterns as described above. The following configuration would only target .lrc files:

filetote:
  pairing:
    enabled: true
    pairing_only: true
    extensions: ".lrc"
Pairing Renaming

To mainting the concept of "pairs" after importing, it is strongly encouraged to set the path for the paired files to use the media files new name. This will ensure thet the file remains paired even after moving. E.g.:

paths:
  paired_ext:.lrc: $albumpath/$medianame_new

Import Operations

This plugin supports the same operations as beets:

  • copy
  • move
  • link (symlink)
  • harklink
  • reflink

These options are mutually exclusive, and there are nuances to how beets (and thus this plugin) behave when there multiple set. See the beets import documentation and #36 for more details.

Reimporting has an additional nuance when copying of linking files that are already in the library, in which files will be moved rather than duplicated. This behavior in Filetote is identical to that of beets. See the beets reimport documentation for more details.

Other CLI Operations

Additional commands such such as move, modify, update, etc. will also trigger Filetote to handle files. These commands typically work with queries, targeting specific files that match the supplied query. Please note that the operation executed by beets for these commands do not use the value set in the config file under import, they instead are specified as part of the CLI command.

Examples of config.yaml

plugins: filetote

paths:
  default: $albumartist/$year - $album/$track - $title
  singleton: Singletons/$artist - $title
  ext:.log: $albumpath/$artist - $album
  ext:.cue: $albumpath/$artist - $album
  paired_ext:.lrc: $albumpath/$medianame_new
  filename:cover.jpg: $albumpath/cover

filetote:
  extensions: .cue .log .jpg
  filename: "cover.jpg"
  pairing:
    enabled: true
    extensions: ".lrc"
  print_ignored: true

Or:

plugins: filetote

paths:
  default: $albumartist/$year - $album/$track - $title
  singleton: Singletons/$artist - $title

filetote:
  extensions: .cue
  patterns:
    artworkdir:
      - "[sS]cans/"
      - "[aA]rtwork/"
  pairing:
    enabled: true
    extensions: ".lrc"
  paths:
    pattern:artworkdir: $albumpath/artwork
    paired_ext:.lrc: $albumpath/$medianame_old
    filename:cover.jpg: $albumpath/cover  

Multi-Disc and Nested Import Directories

beets imports multi-disc albums as a single unit (see beets documentation). By default, this results in the media importing to a single directory in the library. Artifacts and extra files in the initial subdirectories will brought by Filetote to the destination of the file's they're near, resulting in them landing where one would expect. Because of this, the files will also be moved by Filetote to any specified subdirectory in the library if the path definition creates "Disc N" subfolders as described in the beets documentation.

In short, artifacts and extra files in these scenarios should simply just move/copy as expected.

Advanced renaming for multi-disc albums

The value for $albumpath is actually based on the path for the Item (music file) the lead to the artifact to be moved. Since it's common to store multi-disc albums with subfolders, this means that by default the artifact or extra file in question will also be in a subfolder.

For macOS and Linux, to achieve a different location (say the media is in Artist/Disc 01 but artifacts are intended to be in Artist/Extras), .. can be used to navigate to the parent directory of the $albumpath so that the entirety of the media's path does not have to be recreated. For Windows, the entire media path would need to be recreated as Windows sees .. as an attempt to create a directory with the name .. within path instead of it being a path component representing the parent.

macOS & Linux

The following example will have the following results on macOS & Linux:

  • Music: ~/Music/Artist/2014 - Album/Disc 1/media.mp3
  • Artifact: ~/Music/Artist/2014 - Album/Extras/example.log
plugins: filetote

paths:
  default: $albumartist/$year - $album/$track - $title
  comp: $albumartist/$year - $album/Disc $disc/$track - $title

filetote:
  extensions: .log
  paths:
    ext:log: $albumpath/../Extras/$old_filename

Windows

The following example will have the following results on Windows:

  • Music: ~/Music/Artist/2014 - Album/Disc 1/media.mp3
  • Artifact: ~/Music/Artist/2014 - Album/Extras/example.log
plugins: filetote

paths:
  default: $albumartist/$year - $album/$track - $title
  comp: $albumartist/$year - $album/Disc $disc/$track - $title

filetote:
  extensions: .log
  paths:
    ext:log: $albumartist/$year - $album/Extras/$old_filename

Why Filetote and Not Other Plugins?

Filetote serves the same core purpose as the copyfilertifacts plugin and the extrafiles plugin, however both have lacked in maintenance over the last few years. There are outstanding bugs in each (though copyfilertifacts has seen some recent activity resolving some). In addition, each are lacking in certain features and abilities, such as hardlink/reflink support, "paired" file handling, and extending renaming options. What's more, significant focus has been provided to Filetote around Python3 conventions, linting, and typing in order to promote healthier code and easier maintenance.

Filetote strives to encompass all functionality that both copyfilertifacts and extrafiles provide, and then some!

Migrating from copyfilertifacts

Filetote can be configured using nearly identical configuration as copyfilertifacts, simply replacing the name of the plugin in its configuration settings. There is one change that's needed if all extensions are desired, as Filetote does not grab all extensions by default (as copyfilertifacts does). To accommodate, simply explicitly state all extension using .*:

filetote:
  extensions: .*

Otherwise, simply replacing the name in the config section will work. For example:

copyartifacts:
  extensions: .cue .log

Would become:

filetote:
  extensions: .cue .log

Path definitions can also be specified in the way that copyfileartifacts does, alongside other path definitions for beets. E.g.:

paths:
  ext:log: $albumpath/$artist - $album

Migrating from extrafiles

Filetote can be configured using nearly identical configuration as extrafiles, simply replacing the name of the plugin in its configuration settings. For example:

extrafiles:
  patterns:
    all: "*.*"

Would become:

filetote:
  patterns:
    all: "*.*"

Path definitions can also be specified in the way that extrafiles does, e.g.:

filetote:
  patterns:
    artworkdir:
      - '[sS]cans/'
      - '[aA]rtwork/'
  paths:
    artworkdir: $albumpath/artwork

Version Upgrade Instructions

Certain versoins require changes to configurations as upgrades occur. Please see below for specific steps for each version.

0.4.0

Default for extensions is now None

As of version 0.4.0, Filetote no longer set the default for extensions to .*. Instead, setting Filetote to collect all extensions needs to be explicitly defined, e.g.:

filetote:
  extensions: .*

Pairing Config Changes

pairing has been converted from a boolean to an object with other like-config. Take the following config:

filetote:
  pairing: true
  pairing_only: false

These will both now be represented as individual settings within pairing:

filetote:
  pairing:
    enabled: true
    pairing_only: false
    extensions: ".lrc"

Both remain optional and both default to false.

Development

The development version can be installed with Poetry, a Python dependency manager that provides dependency isolation, reproducibility, and streamlined packaging to PyPI.

Testing and linting is performed with Tox.

Filetote currently supports Python 3.6+, which aligns with the most recent version of beets (v1.6.0).

1. Install Poetry & Tox:

python3 -m pip install poetry tox

2. Clone the repository and install the plugin:

git clone https://github.com/gtronset/beets-filetote.git
cd beets-filetote
poetry install

3. Update the config.yaml to utilize the plugin:

pluginpath:
  - /path/to.../beets-filetote/beetsplug

4. Run or test with Poetry (and Tox):

Run beets with the following to locally develop:

poetry run beet

Testing can be run with Tox, ex.:

poetry run tox -e py312

For other linting environments, see tox.ini. Ex: black:

poetry run tox -e black

Docker:

A Docker Compose configuration is available for running the plugin in a controlled environment. Running the docker-compose.yml file for details.

Thanks

This plugin originated as a hard fork from beets-copyartifacts (copyartifacts3).

Thank you to the original work done by Sami Barakat and Adrian Sampson, along with the larger beets community.

Please report any issues you may have and feel free to contribute.

License

Copyright (c) 2022 Gavin Tronset

Licensed under the MIT license.

beets-filetote's People

Contributors

adammillerio avatar dependabot[bot] avatar github-actions[bot] avatar gtronset avatar jlefley avatar mried avatar sbarakat avatar twal avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

beets-filetote's Issues

File stat error

beet imp .                                                                                                                                                                                    in cmd at 18:26:24
Import of the directory:
I:\New_Music\import
was interrupted. Resume (Y/n)?
Resuming interrupted import of I:\New_Music\import

I:\New_Music\import\Emily Brooke\Easy on Me (1 items)
Tagging:
    Emily Brooke - Easy on Me
URL:
    https://www.deezer.com/album/212302112
(Similarity: 96.0%) (source, tracks) (Deezer, 2021, Emily Brooke)
 * Easy on Me -> Easy on Me (source)
[A]pply, More candidates, Skip, Use as-is, as Tracks, Group albums,
Enter search, enter Id, aBort, eDit, edit Candidates?
could not get filesize: [WinError 2] The system cannot find the file specified: '\\\\?\\I:\\New_Music\\import\\Emily Brooke\\Easy on Me\\01 - Easy on Me.mp3'

Disabling filetote I do not get the error.

Sometimes filetote no longer moves the .lrc files either.

Both the music and lyric fille have the same name except for the extension.

Small debug shows the error is in this call beets/library.py

Still to look at why/how filetote is calling that function _getters with the old file

Query: Import of lrc files

Hi,

I have the following relevant config set in my config.yaml, but the lrc files did not get moved:

plugins: 
  filetote
  
paths:
  default: %upper{%asciify{%left{$first_artist,1}}}/$first_artist/($year) $album%aunique{}/$album - $track - $artist - $title
  singleton: Singles/$artist - $title
  albumtype:soundtrack: Soundtracks/($year) $album%aunique{}/$album - $track - $artist - $title
  albumartist:\“Weird\ Al\” Yankovic: W/Weird Al Yankovic/($year) $album%aunique{}/$album - $track - $artist - $title
  albumartist:various\ artists: Various Artists/($year) $album%aunique{}/$album - $track - $artist - $title
  albumartist:various: Various Artists/($year) $album%aunique{}/$album - $track - $artist - $title
  comp: Various Artists/($year) $album%aunique{}/$album - $track - $artist - $title
  ext:.pdf: %upper{%asciify{%left{$first_artist,1}}}/$first_artist/($year) $album%aunique{}/$album - 00 - $artist - $title
  paired_ext:.lrc: %upper{%asciify{%left{$first_artist,1}}}/$first_artist/($year) $album%aunique{}/$artist - $track - $title.lrc

filetote:
  pairing:
    enabled: true
    pairing_only: true
    extensions: ".lrc"
  extensions: .pdf

The original files were named as $track - $title.[mp3|lrc]

What have set up incorrectly?

ModuleNotFoundError: No module named 'typing_extensions'

Problem

When running beet import I get the following error:

** error loading plugin filetote:
Traceback (most recent call last):
  File "/usr/local/lib/python3.9/site-packages/beets/plugins.py", line 268, in load_plugins
    namespace = __import__(modname, None, None)
  File "/usr/local/lib/python3.9/site-packages/beetsplug/filetote.py", line 16, in <module>
    from .filetote_dataclasses import (
  File "/usr/local/lib/python3.9/site-packages/beetsplug/filetote_dataclasses.py", line 22, in <module>
    from typing_extensions import TypeAlias  # pylint: disable=import-error
ModuleNotFoundError: No module named 'typing_extensions'

My expectation was:


Plugin to be loaded correctly.

  • OS: Docker compose / python:3.9-alpine
  • Python version: 3.9.18
  • beets version: 1.6.0
  • Filetote version: 0.4.7 (installed using pip)
  • Turning off other plugins made problem go away (yes/no): no

Wildcard pattern looks in subfolders despite separate declaration

I have Filetote set up like this;

filetote:
  paths:
    pattern:scans: $albumpath/scans/$old_filename
    pattern:cover: $albumpath/${album}_cover
    pattern:cue: $albumpath/${album}_cue
    pattern:log: $albumpath/${album}_log
    pattern:m3u: $albumpath/${album}_m3u
  patterns:
    scans:
      - "[sS]cans/"
    cover:
      - "*.jpg"
    cue:
      - "*.cue"
    log:
      - "*.log"
    m3u:
      - "*.m3u"

Whenever - "[sS]cans/" contains jpg files, the cover pattern above takes precedent. Meaning, when jpg files exist under the scans folder, the scan folder isn't moved. Instead, the individual files end up following this - pattern:cover: $albumpath/${album}_cover.

Is there a declaration or setting I've overlooked to prevent this? Essentially, if jpg files exist under a scans folder - I want that entire folder moved. Any jpg file under the main album folder should follow the *.jpg declaration.

Thanks for the work on this plugin. I really like how it carries over the functionality from extrafiles.

Crash when re-importing library

I found your plugin a few days ago and have enabled so trying to reimport the library items that were imported as-is.

Some of these have extras of lrc and pdf files

Error from pip install:

beet imp -L "data_source::^$"
Traceback (most recent call last):
  File "C:\Python3\Scripts\beet-script.py", line 33, in <module>
    sys.exit(load_entry_point('beets', 'console_scripts', 'beet')())
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "i:\git\beets\beets\ui\__init__.py", line 1301, in main
    _raw_main(args)
  File "i:\git\beets\beets\ui\__init__.py", line 1288, in _raw_main
    subcommand.func(lib, suboptions, subargs)
  File "i:\git\beets\beets\ui\commands.py", line 1034, in import_func
    import_files(lib, paths, query)
  File "i:\git\beets\beets\ui\commands.py", line 974, in import_files
    session.run()
  File "i:\git\beets\beets\importer.py", line 342, in run
    plugins.send('import_begin', session=self)
  File "i:\git\beets\beets\plugins.py", line 488, in send
    result = handler(**arguments)
             ^^^^^^^^^^^^^^^^^^^^
  File "i:\git\beets\beets\plugins.py", line 145, in wrapper
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "C:\Python3\Lib\site-packages\beetsplug\filetote.py", line 94, in _register_session_settings
    self.paths = os.path.expanduser(session.paths[0])
                                    ~~~~~~~~~~~~~^^^
IndexError: list index out of range

Error from git install:

beet -vvv imp -L "data_source::^$" 
user configuration: C:\Users\<username>\AppData\Roaming\beets\config.yaml
data directory: C:\Users\<username>\AppData\Roaming\beets
plugin paths: C:\Users\<username>\AppData\Roaming\beets\beetsplug
inline: adding item field first_artist
inline: adding item field date
inline: adding album field date
lastgenre: Loading canonicalization tree i:\git\beets\beetsplug\lastgenre\genres-tree.yaml
Sending event: pluginload
library database: C:\Users\<username>l\AppData\Roaming\beets\library.db
library directory: I:\Music
Sending event: library_opened
Sending event: import_begin
Traceback (most recent call last):
  File "C:\Python3\Scripts\beet-script.py", line 33, in <module>
    sys.exit(load_entry_point('beets', 'console_scripts', 'beet')())
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "i:\git\beets\beets\ui\__init__.py", line 1301, in main
    _raw_main(args)
  File "i:\git\beets\beets\ui\__init__.py", line 1288, in _raw_main
    subcommand.func(lib, suboptions, subargs)
  File "i:\git\beets\beets\ui\commands.py", line 1034, in import_func
    import_files(lib, paths, query)
  File "i:\git\beets\beets\ui\commands.py", line 974, in import_files
    session.run()
  File "i:\git\beets\beets\importer.py", line 342, in run
    plugins.send('import_begin', session=self)
  File "i:\git\beets\beets\plugins.py", line 488, in send
    result = handler(**arguments)
             ^^^^^^^^^^^^^^^^^^^^
  File "i:\git\beets\beets\plugins.py", line 145, in wrapper
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "C:\Python3\Lib\site-packages\beetsplug\filetote.py", line 100, in _register_session_settings
    self.filetote.session.import_path = os.path.expanduser(session.paths[0])
                                                           ~~~~~~~~~~~~~^^^
IndexError: list index out of range

Config relevant for filetote:

paths:
  ext:.pdf: %upper{%asciify{%left{$first_artist,1}}}/$first_artist/($year) $album%aunique{}/$album - 00 - $artist - $title
  paired_ext:.lrc: %upper{%asciify{%left{$first_artist,1}}}/$first_artist/($year) $album%aunique{}/$artist - $track - $title.rc

filetote:
  pairing:
    enabled: true
    pairing_only: true
    extensions: ".lrc"
  extensions: .pdf

Another bug

When filetote finishes, the empty directories are not deleted:

running beet imp . from dir below

dir
 |   -> sub-dir1
 		-> file.mp3
 		-> file.lrc
|   -> sub-dir2
		-> file2.mp3

leaves the following:

dir
 |   -> sub-dir1

File does not move. Beets moves music files fine. None-ing operation

I changed my beets folder from myfolder to myfolder2 in the same parent folder. this folder does not exist.

Beets did create the folder and move all its music files to it. filetote did not move the toted files.

I also tried moving to a subfolder within myfolder. And I created it before using beets. Beets could move the music but filetote did not move the toted files.

Some permission weirdness may be happening. I did the best I could by chmod +777 before and after the move, but it didn't seem to be enough.

I am using Docker and ARM, running as root within the container which can trigger a lot of edge cases. beetbox/beets#4910

filetote:
    extensions: .*
    # It can also print what got left:
    print_ignored: yes

I notice I can trigger a None-ing operation from the code

self._log.info(
            f"{operation}-ing artifact:"
            f" {os.path.basename(util.displayable_path(artifact_dest))}"
        )

Is None supposed to be reachable normally?

def _operation_type(self) -> MoveOperation:

This person also got none-ing:
#35


root@77ac896c76f0:/#chmod -R +777 /fast/d/music/beetslibrary/ ; beet -vv mv gecs ; chmod -R +777 /fast/d/music/beetslibrary/
user configuration: /config/config.yaml
data directory: /config
plugin paths:
inline: adding item field i_cust_catalog
inline: adding item field i_short_catalog
inline: adding item field i_main
inline: adding album field alb_main
inline: adding album field alb_main_suffix
Sending event: pluginload
library database: /fast/d/music/beetslibrary/beets_library.db
library directory: /fast/d/music/beetslibrary
Sending event: library_opened
Moving 0 items (10 already in place).
Sending event: cli_exit
root@77ac896c76f0:/#chmod -R +777 /fast/d/music/beetslibrary/ ; beet -vv mv gecs ; chmod -R +777 /fast/d/music/beetslibrary/
user configuration: /config/config.yaml
data directory: /config
plugin paths:
inline: adding item field i_cust_catalog
inline: adding item field i_short_catalog
inline: adding item field i_main
inline: adding album field alb_main
inline: adding album field alb_main_suffix
Sending event: pluginload
library database: /fast/d/music/beetslibrary/beets_library.db
library directory: /fast/d/music/beetslibrary/temp
Sending event: library_opened
Moving 10 items.
moving: /fast/d/music/beetslibrary/100 gecs [2019] - 1000 gecs/01. 745 sticky.mp3
Sending event: before_item_moved
Sending event: item_moved
could not get filesize: [Errno 2] No such file or directory: b'/fast/d/music/beetslibrary/100 gecs [2019] - 1000 gecs/01. 745 sticky.mp3'
Sending event: database_change
Sending event: database_change
moving: /fast/d/music/beetslibrary/100 gecs [2019] - 1000 gecs/02. money machine.mp3
Sending event: before_item_moved
Sending event: item_moved
Sending event: database_change
Sending event: database_change
moving: /fast/d/music/beetslibrary/100 gecs [2019] - 1000 gecs/03. 800db cloud.mp3
Sending event: before_item_moved
Sending event: item_moved
Sending event: database_change
Sending event: database_change
moving: /fast/d/music/beetslibrary/100 gecs [2019] - 1000 gecs/04. I Need Help Immediately.mp3
Sending event: before_item_moved
Sending event: item_moved
Sending event: database_change
Sending event: database_change
moving: /fast/d/music/beetslibrary/100 gecs [2019] - 1000 gecs/05. stupid horse.mp3
Sending event: before_item_moved
Sending event: item_moved
Sending event: database_change
Sending event: database_change
moving: /fast/d/music/beetslibrary/100 gecs [2019] - 1000 gecs/06. xXXi_wud_nvrstop_UXXx.mp3
Sending event: before_item_moved
Sending event: item_moved
Sending event: database_change
Sending event: database_change
moving: /fast/d/music/beetslibrary/100 gecs [2019] - 1000 gecs/07. ringtone.mp3
Sending event: before_item_moved
Sending event: item_moved
Sending event: database_change
Sending event: database_change
moving: /fast/d/music/beetslibrary/100 gecs [2019] - 1000 gecs/08. gecgecgec.mp3
Sending event: before_item_moved
Sending event: item_moved
Sending event: database_change
Sending event: database_change
moving: /fast/d/music/beetslibrary/100 gecs [2019] - 1000 gecs/09. hand crushed by a mallet.mp3
Sending event: before_item_moved
Sending event: item_moved
Sending event: database_change
Sending event: database_change
moving: /fast/d/music/beetslibrary/100 gecs [2019] - 1000 gecs/10. gec 2 U.mp3
Sending event: before_item_moved
Sending event: item_moved
Sending event: database_change
Sending event: database_change
Sending event: cli_exit
filetote: None-ing artifact: 00-100_gecs-1000_gecs-(mds003)-web-2019.jpg
Traceback (most recent call last):
  File "/usr/bin/beet", line 8, in <module>
    sys.exit(main())
             ^^^^^^
  File "/usr/lib/python3.11/site-packages/beets/ui/__init__.py", line 1782, in main
    _raw_main(args)
  File "/usr/lib/python3.11/site-packages/beets/ui/__init__.py", line 1771, in _raw_main
    plugins.send('cli_exit', lib=lib)
  File "/usr/lib/python3.11/site-packages/beets/plugins.py", line 488, in send
    result = handler(**arguments)
             ^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/site-packages/beets/plugins.py", line 145, in wrapper
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/site-packages/beetsplug/filetote.py", line 420, in process_events
    self.process_artifacts(
  File "/usr/lib/python3.11/site-packages/beetsplug/filetote.py", line 580, in process_artifacts
    self.manipulate_artifact(artifact_source, artifact_dest)
  File "/usr/lib/python3.11/site-packages/beetsplug/filetote.py", line 655, in manipulate_artifact
    raise AssertionError(f"unknown MoveOperation {operation}")
AssertionError: unknown MoveOperation None

unknown move operation while moving into library

I think you're already aware of this given the TODO in the code, but thought I'd track it anyways

while trying to import an album on:

beets version 1.6.0
Python version 3.10.9
plugins: chroma, convert, discogs, edit, export, fetchart, filetote, ftintitle, info, lastgenre, scrub, zero

my beet config is here and I use this script to import my music from a downloads folder, in particular it calls like:

beet import --move --noincremental --from-scratch ~/Downloads/...

$ ls -1
'01 - Close to your Mind.mp3'
'02 - darling, would you catch me.mp3'
'03 - Acoustic Image.mp3'
'04 - アリスのボサノバ.mp3'
'05 - relative relation (it'\''s all about).mp3'
'06 - Etupirka, Angelica.mp3'
'07 - 白玉茶屋in冥土.mp3'
'08 - ナルキッソスにさよなら.mp3'
'09 - タイニーリトル・アジアンタム.mp3'
'10 - Full Moon Samba.mp3'
'11 - なんてことない日.mp3'
folder.jpg

just added a breakpoint so I could see what may be happening:

diff --git a/beetsplug/filetote.py b/beetsplug/filetote.py
index abd2b17..d010949 100644
--- a/beetsplug/filetote.py
+++ b/beetsplug/filetote.py
@@ -329,6 +329,7 @@ class FiletotePlugin(BeetsPlugin):
                 ignored_files.append(source_file)
                 continue
 
+            breakpoint()
             dest_file = util.unique_path(dest_file)
             util.mkdirall(dest_file)
             dest_file = util.bytestring_path(dest_file)
> /home/sean/.local/lib/python3.10/site-packages/beetsplug/filetote.py(334)process_artifacts()
    333             dest_file = util.unique_path(dest_file)
--> 334             util.mkdirall(dest_file)
    335             dest_file = util.bytestring_path(dest_file)

ipdb> dest_file
'/home/sean/Music/Shibayan Records/TOHO BOSSA NOVA 2/folder.jpg'
> /home/sean/.local/lib/python3.10/site-packages/beetsplug/filetote.py(340)process_artifacts()
    339 
--> 340             self.manipulate_artifact(source_file, dest_file)
    341 

ipdb> n
AssertionError: unknown MoveOperation None

does detect as move properly I think?

ipdb> self._operation_type()
<MoveOperation.MOVE: 0>
ipdb>

just not sure why its None if _operation_type returns MoveOperation.MOVE

ext:.*: $albumpath/artwork/ not working?

Hi thanks for making this!
I'm trying to move all files to /artwork folder except lyrics or cover.jpg.

ext:.*: $albumpath/artwork/$old_filename

Does not seem to work used in this config:

filetote:
  extensions: .*
  patterns:
    artworkdir:
      - "[sS]cans/"
      - "[aA]rtwork/"
      - "[aA]lbum [aA]rt/"
      - "[aA]rt/"
      - "[cC]over/"
      - "[cC]overs/"
      - "[eE]xtra/"
  pairing:
    enabled: true
    extensions: ".lrc"
  paths:
    filename:cover.jpg: $albumpath/cover.jpg
    paired_ext:.lrc: $albumpath/lyrics/$medianame_old
    pattern:artworkdir: $albumpath/artwork/$old_filename
    ext:.*: $albumpath/artwork/$old_filename

What am I doing wrong?

Just realized exclude matches filenames only, would be nice to have it match extensions too as exclude: *.cue outputs an error.

Also unrelated the plugin seems to fail to remove the empty folders when a subdirectory like artwork is present or is that a beets issue?

Update beets version requirement

Hello,

Thanks for actively maintaining this plugin as i'm hoping to move to it from the other older unmaintained options.

Issue i'm facing is that since this requires beets version 1.6.0 it still contains huge bugs like this: beetbox/beets#4528 which has been fixed and can be used with beets-git for example but hasn't had a full release of beets yet.

Can you allow versions which equal that or higher? or any way to not even require a version if possible

Thank you.

  • OS: Arch
  • Python version: 3.11.8
  • beets version: 1.6.0
  • Filetote version: 0.4.6

nothing was moved, many errors about file size and also "TypeError: cannot use a bytes pattern on a string-like object" at end

I just did a huge import and tried the filetote plug-in with this in my config.yaml:

filetote:
  extensions: .*

I thought that would move all non-music files.

Instead, nothing moved, and throughout the import (in which I was using -q and -I, auto tagging and moving to a different folder), I got this error on what appears to be the first track of each album (here is one example):

could not get filesize: [Errno 2] No such file or directory: b'/Users/knewman/beetsmusicsurely/music2023/Music/Yo La Tengo/Painful/01 Big Day Coming.mp3'

Then, at the end of the import,

Traceback (most recent call last):
  File "/Users/knewman/beetsmusicsurely/venv/bin/beet", line 8, in <module>
    sys.exit(main())
             ^^^^^^
  File "/Users/knewman/beetsmusicsurely/venv/lib/python3.11/site-packages/beets/ui/__init__.py", line 1285, in main
    _raw_main(args)
  File "/Users/knewman/beetsmusicsurely/venv/lib/python3.11/site-packages/beets/ui/__init__.py", line 1274, in _raw_main
    plugins.send('cli_exit', lib=lib)
  File "/Users/knewman/beetsmusicsurely/venv/lib/python3.11/site-packages/beets/plugins.py", line 488, in send
    result = handler(**arguments)
             ^^^^^^^^^^^^^^^^^^^^
  File "/Users/knewman/beetsmusicsurely/venv/lib/python3.11/site-packages/beets/plugins.py", line 145, in wrapper
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/Users/knewman/beetsmusicsurely/venv/lib/python3.11/site-packages/beetsplug/filetote.py", line 420, in process_events
    self.process_artifacts(
  File "/Users/knewman/beetsmusicsurely/venv/lib/python3.11/site-packages/beetsplug/filetote.py", line 576, in process_artifacts
    artifact_dest = util.unique_path(artifact_dest)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/knewman/beetsmusicsurely/venv/lib/python3.11/site-packages/beets/util/__init__.py", line 602, in unique_path
    match = re.search(br'\.(\d)+$', base)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/[email protected]/3.11.6/Frameworks/Python.framework/Versions/3.11/lib/python3.11/re/__init__.py", line 176, in search
    return _compile(pattern, flags).search(string)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: cannot use a bytes pattern on a string-like object

Crash on import

    https://www.deezer.com/album/<8-digits>
[A]pply, More candidates, Skip, Use as-is, as Tracks, Group albums,
Enter search, enter Id, aBort, eDit, edit Candidates?
Traceback (most recent call last):
  File "C:\Python3\Lib\site-packages\musicbrainzngs\musicbrainz.py", line 497, in _safe_read
    f = opener.open(req)
        ^^^^^^^^^^^^^^^^
  File "C:\Python3\Lib\urllib\request.py", line 525, in open
    response = meth(req, response)
               ^^^^^^^^^^^^^^^^^^^
  File "C:\Python3\Lib\urllib\request.py", line 634, in http_response
    response = self.parent.error(
               ^^^^^^^^^^^^^^^^^^
  File "C:\Python3\Lib\urllib\request.py", line 563, in error
    return self._call_chain(*args)
           ^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Python3\Lib\urllib\request.py", line 496, in _call_chain
    result = func(*args)
             ^^^^^^^^^^^
  File "C:\Python3\Lib\urllib\request.py", line 643, in http_error_default
    raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 400: Bad Request

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Python3\Scripts\beet-script.py", line 33, in <module>
    sys.exit(load_entry_point('beets', 'console_scripts', 'beet')())
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "i:\git\beets\beets\ui\__init__.py", line 1301, in main
    _raw_main(args)
  File "i:\git\beets\beets\ui\__init__.py", line 1288, in _raw_main
    subcommand.func(lib, suboptions, subargs)
  File "i:\git\beets\beets\ui\commands.py", line 1034, in import_func
    import_files(lib, paths, query)
  File "i:\git\beets\beets\ui\commands.py", line 974, in import_files
    session.run()
  File "i:\git\beets\beets\importer.py", line 345, in run
    pl.run_parallel(QUEUE_SIZE)
  File "i:\git\beets\beets\util\pipeline.py", line 446, in run_parallel
    raise exc_info[1].with_traceback(exc_info[2])
  File "i:\git\beets\beets\util\pipeline.py", line 358, in run
    self.coro.send(msg)
  File "i:\git\beets\beets\util\pipeline.py", line 170, in coro
    task = func(*(args + (task,)))
           ^^^^^^^^^^^^^^^^^^^^^^^
  File "i:\git\beets\beets\importer.py", line 1626, in manipulate_files
    task.manipulate_files(
  File "i:\git\beets\beets\importer.py", line 771, in manipulate_files
    item.move(operation)
  File "i:\git\beets\beets\library.py", line 958, in move
    self.move_file(dest, operation)
  File "i:\git\beets\beets\library.py", line 854, in move_file
    plugins.send("item_moved", item=self, source=self.path,
  File "i:\git\beets\beets\plugins.py", line 488, in send
    result = handler(**arguments)
             ^^^^^^^^^^^^^^^^^^^^
  File "i:\git\beets\beets\plugins.py", line 145, in wrapper
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "I:\Git\beets-filetote\beetsplug\filetote.py", line 390, in collect_artifacts
    mapping=self._generate_mapping(item, destination),
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "I:\Git\beets-filetote\beetsplug\filetote.py", line 292, in _generate_mapping
    mapping_meta.update(beets_item.formatted())
  File "i:\git\beets\beets\library.py", line 453, in __getitem__
    value = self._get(key)
            ^^^^^^^^^^^^^^
  File "i:\git\beets\beets\library.py", line 441, in _get
    return self._get_formatted(self.model, key)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "i:\git\beets\beets\dbcore\db.py", line 88, in _get_formatted
    value = model._type(key).format(model.get(key))
                                    ^^^^^^^^^^^^^^
  File "i:\git\beets\beets\library.py", line 690, in get
    return self._get(key, default, raise_=with_album)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "i:\git\beets\beets\dbcore\db.py", line 373, in _get
    return getters[key](self)
           ^^^^^^^^^^^^^^^^^^
  File "C:\Users\arogl\AppData\Roaming\beets\beetsplug\artistcountry.py", line 15, in memf
    cache[artist_id] = f(item)
                       ^^^^^^^
  File "C:\Users\arogl\AppData\Roaming\beets\beetsplug\artistcountry.py", line 24, in _tmpl_country
    artist_item = get_artist_by_id(item['mb_artistid'])
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Python3\Lib\site-packages\musicbrainzngs\musicbrainz.py", line 821, in get_artist_by_id
    return _do_mb_query("artist", id, includes, params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Python3\Lib\site-packages\musicbrainzngs\musicbrainz.py", line 728, in _do_mb_query
    return _mb_request(path, 'GET', auth_required, args=args)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Python3\Lib\site-packages\musicbrainzngs\musicbrainz.py", line 417, in __call__
    return self.fun(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Python3\Lib\site-packages\musicbrainzngs\musicbrainz.py", line 690, in _mb_request
    resp = _safe_read(opener, req, body)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Python3\Lib\site-packages\musicbrainzngs\musicbrainz.py", line 503, in _safe_read
    raise ResponseError(cause=exc)
musicbrainzngs.musicbrainz.ResponseError: caused by: HTTP Error 400: Bad Request

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.