Merge branch '1060_ft_lang_support'

This commit is contained in:
Adrian Sampson 2014-12-16 11:53:59 +00:00
commit c2184be679
4 changed files with 84 additions and 15 deletions

View file

@ -678,3 +678,17 @@ def max_filename_length(path, limit=MAX_FILENAME_LENGTH):
return min(res[9], limit)
else:
return limit
def feat_tokens(for_artist=True):
"""Return a regular expression that matches phrases like "featuring"
that separate a main artist or a song title from secondary artists.
The `for_artist` option determines whether the regex should be
suitable for matching artist fields (the default) or title fields.
"""
feat_words = ['ft', 'featuring', 'feat', 'feat.', 'ft.']
if for_artist:
feat_words += ['with', 'vs', 'and', 'con', '&']
return '(?<=\s)(?:{0})(?=\s)'.format(
'|'.join(re.escape(x) for x in feat_words)
)

View file

@ -16,7 +16,7 @@
"""
from beets.plugins import BeetsPlugin
from beets import ui
from beets.util import displayable_path
from beets.util import displayable_path, feat_tokens
from beets import config
import logging
import re
@ -30,25 +30,19 @@ def split_on_feat(artist):
artist, which is always a string, and the featuring artist, which
may be a string or None if none is present.
"""
parts = re.split(
r'[fF]t\.|[fF]eaturing|[fF]eat\.|\b[wW]ith\b|&|vs\.|and',
artist,
1, # Only split on the first "feat".
)
parts = [s.strip() for s in parts]
# split on the first "feat".
regex = re.compile(feat_tokens(), re.IGNORECASE)
parts = [s.strip() for s in regex.split(artist, 1)]
if len(parts) == 1:
return parts[0], None
else:
return parts
return tuple(parts)
def contains_feat(title):
"""Determine whether the title contains a "featured" marker.
"""
return bool(re.search(
r'[fF]t\.|[fF]eaturing|[fF]eat\.|\b[wW]ith\b|&',
title,
))
return bool(re.search(feat_tokens(), title, flags=re.IGNORECASE))
def update_metadata(item, feat_part, drop_feat):

View file

@ -29,6 +29,7 @@ from HTMLParser import HTMLParseError
from beets.plugins import BeetsPlugin
from beets import ui
from beets import config
from beets.util import feat_tokens
# Global logger.
@ -137,7 +138,7 @@ def search_pairs(item):
artists = [artist]
# Remove any featuring artists from the artists name
pattern = r"(.*?) (&|\b(and|ft|feat(uring)?\b))"
pattern = r"(.*?) {0}".format(feat_tokens())
match = re.search(pattern, artist, re.IGNORECASE)
if match:
artists.append(match.group(1))
@ -150,8 +151,8 @@ def search_pairs(item):
titles.append(match.group(1))
# Remove any featuring artists from the title
pattern = r"(.*?) \b(ft|feat(uring)?)\b"
for title in titles:
pattern = r"(.*?) {0}".format(feat_tokens(for_artist=False))
for title in titles[:]:
match = re.search(pattern, title, re.IGNORECASE)
if match:
titles.append(match.group(1))

60
test/test_ftintitle.py Normal file
View file

@ -0,0 +1,60 @@
# This file is part of beets.
# Copyright 2014, Fabrice Laporte.
#
# 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.
"""Tests for the 'ftintitle' plugin."""
from _common import unittest
from beetsplug import ftintitle
class FtInTitlePluginTest(unittest.TestCase):
def setUp(self):
"""Set up configuration"""
ftintitle.FtInTitlePlugin()
def test_split_on_feat(self):
parts = ftintitle.split_on_feat('Alice ft. Bob')
self.assertEqual(parts, ('Alice', 'Bob'))
parts = ftintitle.split_on_feat('Alice feat Bob')
self.assertEqual(parts, ('Alice', 'Bob'))
parts = ftintitle.split_on_feat('Alice feat. Bob')
self.assertEqual(parts, ('Alice', 'Bob'))
parts = ftintitle.split_on_feat('Alice featuring Bob')
self.assertEqual(parts, ('Alice', 'Bob'))
parts = ftintitle.split_on_feat('Alice & Bob')
self.assertEqual(parts, ('Alice', 'Bob'))
parts = ftintitle.split_on_feat('Alice and Bob')
self.assertEqual(parts, ('Alice', 'Bob'))
parts = ftintitle.split_on_feat('Alice With Bob')
self.assertEqual(parts, ('Alice', 'Bob'))
parts = ftintitle.split_on_feat('Alice defeat Bob')
self.assertEqual(parts, ('Alice defeat Bob', None))
def test_contains_feat(self):
self.assertTrue(ftintitle.contains_feat('Alice ft. Bob'))
self.assertTrue(ftintitle.contains_feat('Alice feat. Bob'))
self.assertTrue(ftintitle.contains_feat('Alice feat Bob'))
self.assertTrue(ftintitle.contains_feat('Alice featuring Bob'))
self.assertTrue(ftintitle.contains_feat('Alice & Bob'))
self.assertTrue(ftintitle.contains_feat('Alice and Bob'))
self.assertTrue(ftintitle.contains_feat('Alice With Bob'))
self.assertFalse(ftintitle.contains_feat('Alice defeat Bob'))
self.assertFalse(ftintitle.contains_feat('Aliceft.Bob'))
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')