1818
1919import os
2020import re
21+ import typing
2122
22- from beets import plugins
23+ from beets .importer import ImportSession , ImportTask
24+ from beets .library import Item
25+ from beets .plugins import BeetsPlugin
2326from beets .util import displayable_path
2427
2528# Filename field extraction patterns.
4245]
4346
4447
45- def equal (seq ):
48+ def equal (seq : list [ str ] ):
4649 """Determine whether a sequence holds identical elements."""
4750 return len (set (seq )) <= 1
4851
4952
50- def equal_fields (matchdict , field ):
53+ def equal_fields (matchdict : dict [ typing . Any , dict [ str , str ]], field : str ):
5154 """Do all items in `matchdict`, whose values are dictionaries, have
5255 the same value for `field`? (If they do, the field is probably not
5356 the title.)
5457 """
55- return equal (m [field ] for m in matchdict .values ())
58+ return equal (list ( m [field ] for m in matchdict .values () ))
5659
5760
58- def all_matches (names , pattern ):
61+ def all_matches (
62+ names : dict [Item , str ], pattern : str
63+ ) -> dict [Item , dict [str , str ]] | None :
5964 """If all the filenames in the item/filename mapping match the
6065 pattern, return a dictionary mapping the items to dictionaries
6166 giving the value for each named subpattern in the match. Otherwise,
@@ -74,7 +79,7 @@ def all_matches(names, pattern):
7479 return matches
7580
7681
77- def bad_title (title ) :
82+ def bad_title (title : str ) -> bool :
7883 """Determine whether a given title is "bad" (empty or otherwise
7984 meaningless) and in need of replacement.
8085 """
@@ -84,77 +89,28 @@ def bad_title(title):
8489 return False
8590
8691
87- def apply_matches (d , log ):
88- """Given a mapping from items to field dicts, apply the fields to
89- the objects.
90- """
91- some_map = list (d .values ())[0 ]
92- keys = some_map .keys ()
93-
94- # Only proceed if the "tag" field is equal across all filenames.
95- if "tag" in keys and not equal_fields (d , "tag" ):
96- return
97-
98- # Given both an "artist" and "title" field, assume that one is
99- # *actually* the artist, which must be uniform, and use the other
100- # for the title. This, of course, won't work for VA albums.
101- # Only check for "artist": patterns containing it, also contain "title"
102- if "artist" in keys :
103- if equal_fields (d , "artist" ):
104- artist = some_map ["artist" ]
105- title_field = "title"
106- elif equal_fields (d , "title" ):
107- artist = some_map ["title" ]
108- title_field = "artist"
109- else :
110- # Both vary. Abort.
111- return
112-
113- for item in d :
114- if not item .artist :
115- item .artist = artist
116- log .info ("Artist replaced with: {.artist}" , item )
117- # otherwise, if the pattern contains "title", use that for title_field
118- elif "title" in keys :
119- title_field = "title"
120- else :
121- title_field = None
122-
123- # Apply the title and track, if any.
124- for item in d :
125- if title_field and bad_title (item .title ):
126- item .title = str (d [item ][title_field ])
127- log .info ("Title replaced with: {.title}" , item )
128-
129- if "track" in d [item ] and item .track == 0 :
130- item .track = int (d [item ]["track" ])
131- log .info ("Track replaced with: {.track}" , item )
132-
133-
134- # Plugin structure and hook into import process.
135-
136-
137- class FromFilenamePlugin (plugins .BeetsPlugin ):
138- def __init__ (self ):
92+ class FromFilenamePlugin (BeetsPlugin ):
93+ def __init__ (self ) -> None :
13994 super ().__init__ ()
14095 self .register_listener ("import_task_start" , self .filename_task )
14196
142- def filename_task (self , task , session ) :
97+ def filename_task (self , task : ImportTask , session : ImportSession ) -> None :
14398 """Examine each item in the task to see if we can extract a title
14499 from the filename. Try to match all filenames to a number of
145100 regexps, starting with the most complex patterns and successively
146101 trying less complex patterns. As soon as all filenames match the
147102 same regex we can make an educated guess of which part of the
148103 regex that contains the title.
149104 """
150- items = task .items if task .is_album else [task .item ]
105+ # Create the list of items to process
106+ items : list [Item ] = task .items
151107
152108 # Look for suspicious (empty or meaningless) titles.
153109 missing_titles = sum (bad_title (i .title ) for i in items )
154110
155111 if missing_titles :
156112 # Get the base filenames (no path or extension).
157- names = {}
113+ names : dict [ Item , str ] = {}
158114 for item in items :
159115 path = displayable_path (item .path )
160116 name , _ = os .path .splitext (os .path .basename (path ))
@@ -163,6 +119,51 @@ def filename_task(self, task, session):
163119 # Look for useful information in the filenames.
164120 for pattern in PATTERNS :
165121 self ._log .debug (f"Trying pattern: { pattern } " )
166- d = all_matches (names , pattern )
167- if d :
168- apply_matches (d , self ._log )
122+ if d := all_matches (names , pattern ):
123+ self ._apply_matches (d )
124+
125+ def _apply_matches (self , d : dict [Item , dict [str , str ]]) -> None :
126+ """Given a mapping from items to field dicts, apply the fields to
127+ the objects.
128+ """
129+ some_map = list (d .values ())[0 ]
130+ keys = some_map .keys ()
131+
132+ # Only proceed if the "tag" field is equal across all filenames.
133+ if "tag" in keys and not equal_fields (d , "tag" ):
134+ return
135+
136+ # Given both an "artist" and "title" field, assume that one is
137+ # *actually* the artist, which must be uniform, and use the other
138+ # for the title. This, of course, won't work for VA albums.
139+ # Only check for "artist": patterns containing it, also contain "title"
140+ if "artist" in keys :
141+ if equal_fields (d , "artist" ):
142+ artist = some_map ["artist" ]
143+ title_field = "title"
144+ elif equal_fields (d , "title" ):
145+ artist = some_map ["title" ]
146+ title_field = "artist"
147+ else :
148+ # Both vary. Abort.
149+ return
150+
151+ for item in d :
152+ if not item .artist :
153+ item .artist = artist
154+ self ._log .info (f"Artist replaced with: { item .artist } " )
155+ # otherwise, if the pattern contains "title", use that for title_field
156+ elif "title" in keys :
157+ title_field = "title"
158+ else :
159+ title_field = None
160+
161+ # Apply the title and track, if any.
162+ for item in d :
163+ if title_field and bad_title (item .title ):
164+ item .title = str (d [item ][title_field ])
165+ self ._log .info (f"Title replaced with: { item .title } " )
166+
167+ if "track" in d [item ] and item .track == 0 :
168+ item .track = int (d [item ]["track" ])
169+ self ._log .info (f"Track replaced with: { item .track } " )
0 commit comments