Repository: beetbox/beets

Issue:
fromfilename: Fails to detect `<track> <title>` filename patterns
<!--
Describe your problem, feature request, or discussion topic here.

Please fill out this and the "Setup" section below and remember to include
enough detail so that other people can reproduce the problem.
-->

### Problem

Running this command in verbose (`-vv`) mode:

```sh
$ beet import path/to/unknown_music_tracks
path/to/unknown_music_tracks (5 items)
No matching release found for 5 tracks.
For help, see: https://beets.readthedocs.org/en/latest/faq.html#nomatch
[S]kip, Use as-is, as Tracks, Group albums, Enter search, enter Id, aBort?
$ u
```

to Use as-is, will led to this problem:

the track numbers are not recognized. For instance, if I run `beet ls`:

```
$ beet ls
 -  - 01 深き森にて
 -  - 02 緋龍の翼
 -  - 03 風の国ルイヴェスタ
 -  - 04 終りなき狂詩曲
 -  - 05 無慈悲な夜に
```

the track numbers are still part of the files.

If I disable fromfilename plugin, the whole output is empty (no track number nor track name).

Here's a link to the music files that trigger the bug (if relevant):

Use any unknown track, such as a dummy sound file you would create, or rename the folder/files so artist/track cannot be identified (this is however a real use case; I'm simply downloading lesser known tracks from indie bands, etc.)

### Setup

* OS: Ubuntu 20.04
* Python version: 3.8.10
* beets version: 1.6.0
* Turning off plugins made problem go away (yes/no): no

<!--
You can turn off plugins temporarily by passing --plugins= on the command line:

$ beet --plugins= version
-->

My configuration (output of `beet config`) is:

```yaml
directory: ~/path/to/Music
import:
    copy: no
    write: no
plugins: fromfilename
```

Note: write: yes didn't help. In fact, it didn't even seem to change the track names, but that's another issue. I assume `beet ls` alone should show expected information.


Discussion:
Can you please include the filenames you're importing? The `fromfilename` plugin relies on very specific patterns in the names to detect various fields.
Folder: 風の国ルイヴェスタ
File names:
01 深き森にて.ogg
02 緋龍の翼.ogg
03 風の国ルイヴェスタ.ogg
04 終りなき狂詩曲.ogg
05 無慈悲な夜に.ogg

(more exactly, I tried both with underscore "01_[name].ogg" which was the original name, and space "01 [name].ogg")
Thanks for the extra info!

Something does indeed look odd in our `fromfilename` list of patterns. We try each pattern sequentially, in order:
https://github.com/beetbox/beets/blob/7cfb55b74657c7d31dba9f5c6dc4cfc404fc7379/beetsplug/fromfilename.py#L159

However, our patterns appear in this order:
https://github.com/beetbox/beets/blob/7cfb55b74657c7d31dba9f5c6dc4cfc404fc7379/beetsplug/fromfilename.py#L32-L33

That is, we try considering the _entire_ filename as _just_ the title first—and only if that doesn't work, we try splitting it into a track number and a title. It _really_ seems like this should go in the other order.

If you have a moment, any chance you could give this a try with those two lines swapped?
Interestingly, that ordering has been in place for a really long time by now: https://github.com/beetbox/beets/pull/399/files
> If you have a moment, any chance you could give this a try with those two lines swapped?

Yes, it works with "<track>[\s.-_]<title>" before "<title>". I also noticed the underscore is recognized as valid separator in this pattern, so I tried again with the original file names, and it works (I had to use a custom format to display the track numbers):

In the folder containing the tracks:

```sh
$ beet import .

path/to/unknown_music_tracks (5 items)
No matching release found for 5 tracks.
For help, see: https://beets.readthedocs.org/en/latest/faq.html#nomatch
[S]kip, Use as-is, as Tracks, Group albums, Enter search, enter Id, aBort? u
$ beet ls
 -  - 深き森にて
 -  - 緋龍の翼
 -  - 風の国ルイヴェスタ
 -  - 終りなき狂詩曲
 -  - 無慈悲な夜に
$ beet ls -f '$album - $artist - $track : $title'
 -  - 01 : 深き森にて
 -  - 02 : 緋龍の翼
 -  - 03 : 風の国ルイヴェスタ
 -  - 04 : 終りなき狂詩曲
 -  - 05 : 無慈悲な夜に
```

So with this, I just need to learn how to apply beet library metadata back to the actual track files, and I should be able to update my music tags very fast!

I'll let you have a look whether there are other lines to reorder while we're at it though, and if the reordering won't have bad side effects we didn't think of (isn't there a list of unit tests with dummy track names to test on?)
Indeed; I can't think of any bad side effects—we should probably just do the reordering!

Resolving patch:
diff --git a/beetsplug/fromfilename.py b/beetsplug/fromfilename.py
--- a/beetsplug/fromfilename.py
+++ b/beetsplug/fromfilename.py
@@ -21,7 +21,6 @@
 import os
 import re
 
-
 # Filename field extraction patterns.
 PATTERNS = [
   # Useful patterns.
@@ -29,11 +28,11 @@
   r'^(?P<track>\d+)[\s.\-_]+(?P<artist>.+)[\-_](?P<title>.+)[\-_](?P<tag>.*)$',
   r'^(?P<artist>.+)[\-_](?P<title>.+)$',
   r'^(?P<track>\d+)[\s.\-_]+(?P<artist>.+)[\-_](?P<title>.+)$',
-  r'^(?P<title>.+)$',
   r'^(?P<track>\d+)[\s.\-_]+(?P<title>.+)$',
   r'^(?P<track>\d+)\s+(?P<title>.+)$',
   r'^(?P<title>.+) by (?P<artist>.+)$',
   r'^(?P<track>\d+).*$',
+  r'^(?P<title>.+)$',
 ]
 
 # Titles considered "empty" and in need of replacement.
@@ -85,7 +84,7 @@ def bad_title(title):
     return False
 
 
-def apply_matches(d):
+def apply_matches(d, log):
     """Given a mapping from items to field dicts, apply the fields to
     the objects.
     """
@@ -113,6 +112,7 @@ def apply_matches(d):
         for item in d:
             if not item.artist:
                 item.artist = artist
+                log.info('Artist replaced with: {}'.format(item.artist))
 
     # No artist field: remaining field is the title.
     else:
@@ -122,8 +122,11 @@ def apply_matches(d):
     for item in d:
         if bad_title(item.title):
             item.title = str(d[item][title_field])
+            log.info('Title replaced with: {}'.format(item.title))
+
         if 'track' in d[item] and item.track == 0:
             item.track = int(d[item]['track'])
+            log.info('Track replaced with: {}'.format(item.track))
 
 
 # Plugin structure and hook into import process.
@@ -131,32 +134,31 @@ def apply_matches(d):
 class FromFilenamePlugin(plugins.BeetsPlugin):
     def __init__(self):
         super().__init__()
-        self.register_listener('import_task_start', filename_task)
-
-
-def filename_task(task, session):
-    """Examine each item in the task to see if we can extract a title
-    from the filename. Try to match all filenames to a number of
-    regexps, starting with the most complex patterns and successively
-    trying less complex patterns. As soon as all filenames match the
-    same regex we can make an educated guess of which part of the
-    regex that contains the title.
-    """
-    items = task.items if task.is_album else [task.item]
-
-    # Look for suspicious (empty or meaningless) titles.
-    missing_titles = sum(bad_title(i.title) for i in items)
-
-    if missing_titles:
-        # Get the base filenames (no path or extension).
-        names = {}
-        for item in items:
-            path = displayable_path(item.path)
-            name, _ = os.path.splitext(os.path.basename(path))
-            names[item] = name
-
-        # Look for useful information in the filenames.
-        for pattern in PATTERNS:
-            d = all_matches(names, pattern)
-            if d:
-                apply_matches(d)
+        self.register_listener('import_task_start', self.filename_task)
+
+    def filename_task(self, task, session):
+        """Examine each item in the task to see if we can extract a title
+        from the filename. Try to match all filenames to a number of
+        regexps, starting with the most complex patterns and successively
+        trying less complex patterns. As soon as all filenames match the
+        same regex we can make an educated guess of which part of the
+        regex that contains the title.
+        """
+        items = task.items if task.is_album else [task.item]
+
+        # Look for suspicious (empty or meaningless) titles.
+        missing_titles = sum(bad_title(i.title) for i in items)
+
+        if missing_titles:
+            # Get the base filenames (no path or extension).
+            names = {}
+            for item in items:
+                path = displayable_path(item.path)
+                name, _ = os.path.splitext(os.path.basename(path))
+                names[item] = name
+
+            # Look for useful information in the filenames.
+            for pattern in PATTERNS:
+                d = all_matches(names, pattern)
+                if d:
+                    apply_matches(d, self._log)


Test patch: