Skip to content

Commit

Permalink
Merge branch 'master' into atomic-move-fix
Browse files Browse the repository at this point in the history
  • Loading branch information
sampsyo authored Jan 11, 2022
2 parents 6868388 + 28ceda1 commit 1962223
Show file tree
Hide file tree
Showing 16 changed files with 360 additions and 30 deletions.
28 changes: 17 additions & 11 deletions beetsplug/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,17 +514,23 @@ def convert_on_import(self, lib, item):
except subprocess.CalledProcessError:
return

# Change the newly-imported database entry to point to the
# converted file.
source_path = item.path
item.path = dest
item.write()
item.read() # Load new audio information data.
item.store()

if self.config['delete_originals']:
self._log.info('Removing original file {0}', source_path)
util.remove(source_path, False)
pretend = self.config['pretend'].get(bool)
quiet = self.config['quiet'].get(bool)

if not pretend:
# Change the newly-imported database entry to point to the
# converted file.
source_path = item.path
item.path = dest
item.write()
item.read() # Load new audio information data.
item.store()

if self.config['delete_originals']:
if not quiet:
self._log.info('Removing original file {0}',
source_path)
util.remove(source_path, False)

def _cleanup(self, task, session):
for path in task.old_paths:
Expand Down
5 changes: 5 additions & 0 deletions beetsplug/discogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@ def candidates(self, items, artist, album, va_likely, extra_tags=None):
if not self.discogs_client:
return

if not album and not artist:
self._log.debug('Skipping Discogs query. Files missing album and '
'artist tags.')
return []

if va_likely:
query = album
else:
Expand Down
101 changes: 101 additions & 0 deletions beetsplug/limit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# This file is part of beets.
#
# 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.

"""Adds head/tail functionality to list/ls.
1. Implemented as `lslimit` command with `--head` and `--tail` options. This is
the idiomatic way to use this plugin.
2. Implemented as query prefix `<` for head functionality only. This is the
composable way to use the plugin (plays nicely with anything that uses the
query language).
"""

from beets.dbcore import FieldQuery
from beets.plugins import BeetsPlugin
from beets.ui import Subcommand, decargs, print_
from collections import deque
from itertools import islice


def lslimit(lib, opts, args):
"""Query command with head/tail."""

if (opts.head is not None) and (opts.tail is not None):
raise ValueError("Only use one of --head and --tail")
if (opts.head or opts.tail or 0) < 0:
raise ValueError("Limit value must be non-negative")

query = decargs(args)
if opts.album:
objs = lib.albums(query)
else:
objs = lib.items(query)

if opts.head is not None:
objs = islice(objs, opts.head)
elif opts.tail is not None:
objs = deque(objs, opts.tail)

for obj in objs:
print_(format(obj))


lslimit_cmd = Subcommand(
"lslimit",
help="query with optional head or tail"
)

lslimit_cmd.parser.add_option(
"--head",
action="store",
type="int",
default=None
)

lslimit_cmd.parser.add_option(
"--tail",
action="store",
type="int",
default=None
)

lslimit_cmd.parser.add_all_common_options()
lslimit_cmd.func = lslimit


class LimitPlugin(BeetsPlugin):
"""Query limit functionality via command and query prefix."""

def commands(self):
"""Expose `lslimit` subcommand."""
return [lslimit_cmd]

def queries(self):

class HeadQuery(FieldQuery):
"""This inner class pattern allows the query to track state."""
n = 0
N = None

@classmethod
def value_match(cls, pattern, value):
if cls.N is None:
cls.N = int(pattern)
if cls.N < 0:
raise ValueError("Limit value must be non-negative")
cls.n += 1
return cls.n <= cls.N

return {
"<": HeadQuery
}
10 changes: 8 additions & 2 deletions beetsplug/lyrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,11 +419,17 @@ def _scrape_lyrics_from_html(self, html):
lyrics_div = verse_div.parent
for br in lyrics_div.find_all("br"):
br.replace_with("\n")

ads = lyrics_div.find_all("div",
class_=re.compile("InreadAd__Container"))
for ad in ads:
ad.replace_with("\n")

footers = lyrics_div.find_all("div",
class_=re.compile("Lyrics__Footer"))
for footer in footers:
footer.replace_with("")

return lyrics_div.get_text()


Expand Down Expand Up @@ -488,11 +494,11 @@ def extract_lyrics(self, html):
if not soup:
return None

lyrics_div = soup.find("div", class_="song-text")
lyrics_div = soup.select("div.song-text > div.inner-text")
if not lyrics_div:
return None

return lyrics_div.get_text()
return lyrics_div[0].get_text()


def remove_credits(text):
Expand Down
2 changes: 1 addition & 1 deletion beetsplug/web/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ def to_python(self, value):
for query in queries]

def to_url(self, value):
return ','.join([v.replace(os.sep, '\\') for v in value])
return '/'.join([v.replace(os.sep, '\\') for v in value])


class EverythingConverter(PathConverter):
Expand Down
18 changes: 17 additions & 1 deletion docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Bug fixes:
* :doc:`/plugins/unimported`: The new ``ignore_subdirectories`` configuration
option added in 1.6.0 now has a default value if it hasn't been set.
* :doc:`/plugins/deezer`: Tolerate missing fields when searching for singleton
tracks
tracks.
:bug:`4116`
* :doc:`/plugins/replaygain`: The type of the internal ``r128_track_gain`` and
``r128_album_gain`` fields was changed from integer to float to fix loss of
Expand All @@ -31,13 +31,29 @@ Bug fixes:
* Fix a regression in the previous release that caused a `TypeError` when
moving files across filesystems.
:bug:`4168`
* :doc:`/plugins/convert`: Files are no longer converted when running import in
``--pretend`` mode.
* :doc:`/plugins/convert`: Deleting the original files during conversion no
longer logs output when the ``quiet`` flag is enabled.
* :doc:`plugins/web`: Fix handling of "query" requests. Previously queries
consisting of more than one token (separated by a slash) always returned an
empty result.
* :doc:`/plugins/discogs`: Skip Discogs query on insufficiently tagged files
(artist and album tags missing) to prevent arbitrary candidate results.
:bug:`4227`
* :doc:`plugins/lyrics`: Fixed issues with the Tekstowo.pl and Genius
backends where some non-lyrics content got included in the lyrics

For packagers:

* We fixed a version for the dependency on the `Confuse`_ library.
:bug:`4167`
* The minimum required version of :pypi:`mediafile` is now 0.9.0.

Other new things:

* :doc:`/plugins/limit`: Limit query results to head or tail (``lslimit``
command only)

1.6.0 (November 27, 2021)
-------------------------
Expand Down
3 changes: 2 additions & 1 deletion docs/plugins/discogs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ authentication credentials via a personal access token or an OAuth2
authorization.

Matches from Discogs will now show up during import alongside matches from
MusicBrainz.
MusicBrainz. The search terms sent to the Discogs API are based on the artist
and album tags of your tracks. If those are empty no query will be issued.

If you have a Discogs ID for an album you want to tag, you can also enter it
at the "enter Id" prompt in the importer.
Expand Down
1 change: 1 addition & 0 deletions docs/plugins/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ following to your configuration::
kodiupdate
lastgenre
lastimport
limit
loadext
lyrics
mbcollection
Expand Down
58 changes: 58 additions & 0 deletions docs/plugins/limit.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
Limit Query Plugin
==================

``limit`` is a plugin to limit a query to the first or last set of
results. We also provide a query prefix ``'<n'`` to inline the same
behavior in the ``list`` command. They are analagous to piping results:

$ beet [list|ls] [QUERY] | [head|tail] -n n

There are two provided interfaces:

1. ``beet lslimit [--head n | --tail n] [QUERY]`` returns the head or
tail of a query

2. ``beet [list|ls] [QUERY] '<n'`` returns the head of a query

There are two differences in behavior:

1. The query prefix does not support tail.

2. The query prefix could appear anywhere in the query but will only
have the same behavior as the ``lslimit`` command and piping to ``head``
when it appears last.

Performance for the query previx is much worse due to the current
singleton-based implementation.

So why does the query prefix exist? Because it composes with any other
query-based API or plugin (see :doc:`/reference/query`). For example,
you can use the query prefix in ``smartplaylist``
(see :doc:`/plugins/smartplaylist`) to limit the number of tracks in a smart
playlist for applications like most played and recently added.

Configuration
=============

Enable the ``limit`` plugin in your configuration (see
:ref:`using-plugins`).

Examples
========

First 10 tracks

$ beet ls | head -n 10
$ beet lslimit --head 10
$ beet ls '<10'

Last 10 tracks

$ beet ls | tail -n 10
$ beet lslimit --tail 10

100 mostly recently released tracks

$ beet lslimit --head 100 year- month- day-
$ beet ls year- month- day- '<100'
$ beet lslimit --tail 100 year+ month+ day+
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ per-file-ignores =
./beetsplug/permissions.py:D
./beetsplug/spotify.py:D
./beetsplug/lastgenre/__init__.py:D
./beetsplug/limit.py:D
./beetsplug/mbcollection.py:D
./beetsplug/metasync/amarok.py:D
./beetsplug/metasync/itunes.py:D
Expand Down Expand Up @@ -161,6 +162,7 @@ per-file-ignores =
./test/test_library.py:D
./test/test_ui_commands.py:D
./test/test_lyrics.py:D
./test/test_limit.py:D
./test/test_beatport.py:D
./test/test_random.py:D
./test/test_embyupdate.py:D
Expand Down
30 changes: 25 additions & 5 deletions test/test_convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ def test_import_converted(self):
item = self.lib.items().get()
self.assertFileTag(item.path, 'convert')

@unittest.skipIf(sys.platform, 'win32') # FIXME: fails on windows
# FIXME: fails on windows
@unittest.skipIf(sys.platform == 'win32', 'win32')
def test_import_original_on_convert_error(self):
# `false` exits with non-zero code
self.config['convert']['command'] = 'false'
Expand All @@ -122,9 +123,28 @@ def test_delete_originals(self):
self.importer.run()
for path in self.importer.paths:
for root, dirnames, filenames in os.walk(path):
self.assertTrue(len(fnmatch.filter(filenames, '*.mp3')) == 0,
'Non-empty import directory {}'
.format(util.displayable_path(path)))
self.assertEqual(len(fnmatch.filter(filenames, '*.mp3')), 0,
'Non-empty import directory {}'
.format(util.displayable_path(path)))

def test_delete_originals_keeps_originals_when_pretend_enabled(self):
import_file_count = self.get_count_of_import_files()

self.config['convert']['delete_originals'] = True
self.config['convert']['pretend'] = True
self.importer.run()

self.assertEqual(self.get_count_of_import_files(), import_file_count,
'Count of files differs after running import')

def get_count_of_import_files(self):
import_file_count = 0

for path in self.importer.paths:
for root, _, filenames in os.walk(path):
import_file_count += len(filenames)

return import_file_count


class ConvertCommand:
Expand Down Expand Up @@ -264,7 +284,7 @@ def tearDown(self):
self.unload_plugins()
self.teardown_beets()

def test_transcode_from_lossles(self):
def test_transcode_from_lossless(self):
[item] = self.add_item_fixtures(ext='flac')
with control_stdin('y'):
self.run_convert_path(item.path)
Expand Down
Loading

0 comments on commit 1962223

Please sign in to comment.