Skip to content

Commit

Permalink
Merge 0ed4a84 into b4a7865
Browse files Browse the repository at this point in the history
  • Loading branch information
wisp3rwind committed Mar 18, 2020
2 parents b4a7865 + 0ed4a84 commit bc21ba9
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 16 deletions.
97 changes: 81 additions & 16 deletions beetsplug/alternatives.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,34 @@
from beetsplug import convert


def _remove(path, soft=True):
"""Remove the file. If `soft`, then no error will be raised if the
file does not exist.
In contrast to beets' util.remove, this uses lexists such that it can
actually remove symlink links.
"""
path = syspath(path)
if soft and not os.path.lexists(path):
return
try:
os.remove(path)
except (OSError, IOError) as exc:
raise FilesystemError(exc, 'delete', (path,), traceback.format_exc())

def _samelink(p1, p2):
if p1 == p2:
return True
else:
# os.path.samefile / shutil._samefile follow symlinks, which is
# not really what we want to check here (although it will typically
# work, because if path and dest are not identical, then dest will
# not exist).
try:
return os.path.samestat(os.lstat(p1), os.lstat(p2))
except OSError:
return False


class AlternativesPlugin(BeetsPlugin):

def __init__(self):
Expand Down Expand Up @@ -166,29 +194,43 @@ def parse_config(self, config):
dir = os.path.join(self.lib.directory, dir)
self.directory = dir

def is_in_external(self, path):
return os.path.isfile(syspath(path))

def item_change_actions(self, item, path, dest):
""" Returns the necessary actions for items that were previously in the
external collection, but might require metadata updates.
"""
actions = []

if not util.samefile(path, dest):
actions.append(self.MOVE)

item_mtime_alt = os.path.getmtime(syspath(path))
if (item_mtime_alt < os.path.getmtime(syspath(item.path))):
actions.append(self.WRITE)
album = item.get_album()

if album:
if (album.artpath and
os.path.isfile(syspath(album.artpath)) and
(item_mtime_alt
< os.path.getmtime(syspath(album.artpath)))):
actions.append(self.SYNC_ART)

return actions

def matched_item_action(self, item):
path = self.get_path(item)
if path and os.path.isfile(syspath(path)):
if path and self.is_in_external(path):
dest = self.destination(item)
_, path_ext = os.path.splitext(path)
_, dest_ext = os.path.splitext(dest)
if not path_ext == dest_ext:
# formats config option changed
return (item, [self.REMOVE, self.ADD])
actions = []
if not util.samefile(path, dest):
actions.append(self.MOVE)
item_mtime_alt = os.path.getmtime(syspath(path))
if (item_mtime_alt < os.path.getmtime(syspath(item.path))):
actions.append(self.WRITE)
album = item.get_album()
if album:
if (album.artpath and
os.path.isfile(syspath(album.artpath)) and
(item_mtime_alt
< os.path.getmtime(syspath(album.artpath)))):
actions.append(self.SYNC_ART)
return (item, actions)
else:
return (item, self.item_change_actions(item, path, dest))
else:
return (item, [self.ADD])

Expand Down Expand Up @@ -276,7 +318,7 @@ def get_path(self, item):

def remove_item(self, item):
path = self.get_path(item)
util.remove(path)
_remove(path)
util.prune_dirs(path, root=self.directory)
del item[self.path_key]

Expand Down Expand Up @@ -359,6 +401,25 @@ def parse_config(self, config):

super(SymlinkView, self).parse_config(config)

def is_in_external(self, path):
return os.path.islink(syspath(path))

def item_change_actions(self, item, path, dest):
""" Returns the necessary actions for items that were previously in the
external collection, but might require metadata updates.
"""
actions = []

if not _samelink(path, dest):
# The path of the link itself changed
actions.append(self.MOVE)
elif not util.samefile(path, item.path):
# link target changed
actions.append(self.MOVE)

# FIXME: test album art link
return actions

def update(self, create=None):
for (item, actions) in self.items_actions():
dest = self.destination(item)
Expand Down Expand Up @@ -389,6 +450,10 @@ def create_symlink(self, item):
if self.relativelinks == self.LINK_RELATIVE else item.path)
util.link(link, dest)

def sync_art(self, item, path):
# FIXME: symlink art
pass


class Worker(futures.ThreadPoolExecutor):

Expand Down
38 changes: 38 additions & 0 deletions test/cli_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,44 @@ def test_add_move_remove_album_relative(self):
self.alt_config['link_type'] = 'relative'
self.test_add_move_remove_album(absolute=False)

def test_add_update_move_album(self, absolute=True):
""" Test that symlinks are properly updated and no broken links left
when an item's path in the library changes.
Since moving the items causes the links in the symlink view to be
broken, this situation used to be incorrectly detected as
addition of as new items, such that the old links weren't removed.
Contrast this to the `test_add_move_remove_album` test, in which the
old links do not break upon changing the path format.
* An album is added.
* The album name is changed, which also causes the tracks to be moved.
* The symlink view is updated.
"""
self.add_album(artist='Michael Jackson', album='Thriller', year='1990')

self.runcli('alt', 'update', 'by-year')

by_year_path = self.lib_path(b'by-year/1990/Thriller/track 1.mp3')
self.assertSymlink(
link=by_year_path,
target=self.lib_path(b'Michael Jackson/Thriller/track 1.mp3'),
absolute=absolute,
)

# `-y` skips the prompt, `-a` updates album-level fields, `-m` forces
# actually moving the files
self.runcli('mod', '-y', '-a', '-m',
'Thriller', 'album=Thriller (Remastered)')
self.runcli('alt', 'update', 'by-year')

self.assertIsNotFile(by_year_path)
self.assertSymlink(
link=self.lib_path(
b'by-year/1990/Thriller (Remastered)/track 1.mp3'),
target=self.lib_path(
b'Michael Jackson/Thriller (Remastered)/track 1.mp3'),
absolute=absolute,
)

def test_valid_options(self):
""" Test that an error is raised when option is invalid
* Config link type is invalid
Expand Down

0 comments on commit bc21ba9

Please sign in to comment.