From 430369062bf899b8a650e0f8eace011c53c86e4a Mon Sep 17 00:00:00 2001 From: anshuman73 Date: Tue, 29 Nov 2016 00:28:38 +0530 Subject: [PATCH 01/71] Fixed a small typo --- beetsplug/mbcollection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/mbcollection.py b/beetsplug/mbcollection.py index a590a76059..992c51fe3b 100644 --- a/beetsplug/mbcollection.py +++ b/beetsplug/mbcollection.py @@ -80,7 +80,7 @@ def imported(self, session, task): self.update_album_list([task.album]) def update_album_list(self, album_list): - """Update the MusicBrainz colleciton from a list of Beets albums + """Update the MusicBrainz collection from a list of Beets albums """ # Get the available collections. collections = mb_call(musicbrainzngs.get_collections) From 318661b4a90ef61c328587eb8dc74f607daf5d98 Mon Sep 17 00:00:00 2001 From: anshuman73 Date: Tue, 29 Nov 2016 01:53:59 +0530 Subject: [PATCH 02/71] Added parsing of secondary album types. Fixes #2200 --- beets/autotag/mb.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index 78d382d878..6ce75772c7 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -276,11 +276,18 @@ def album_info(release): disambig.append(release.get('disambiguation')) info.albumdisambig = u', '.join(disambig) - # Release type not always populated. - if 'type' in release['release-group']: - reltype = release['release-group']['type'] - if reltype: - info.albumtype = reltype.lower() + # Considers all release types (both primary and secondary) and stores as a comma-separated string + all_types = [] + if 'primary-type' in release['release-group']: + rel_primarytype = release['release-group']['primary-type'] + if rel_primarytype: + all_types.append(rel_primarytype.lower()) + if 'secondary-type-list' in release['release-group']: + for secondarytype in release['release-group']['secondary-type-list']: + all_types.append(secondarytype.lower()) + if len(all_types) != 0: + all_types = ','.join(all_types) + info.albumtype = all_types.lower() # Release dates. release_date = release.get('date') From a9d9d83660fccce266dd439567ccce62f803104d Mon Sep 17 00:00:00 2001 From: anshuman73 Date: Tue, 29 Nov 2016 10:05:17 +0530 Subject: [PATCH 03/71] Added support for type field and made code more elegant --- beets/autotag/mb.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index 6ce75772c7..452aa7d506 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -276,18 +276,22 @@ def album_info(release): disambig.append(release.get('disambiguation')) info.albumdisambig = u', '.join(disambig) - # Considers all release types (both primary and secondary) and stores as a comma-separated string + # Considers all release types (both primary and secondary) and + # stores them as a comma-separated string all_types = [] if 'primary-type' in release['release-group']: rel_primarytype = release['release-group']['primary-type'] if rel_primarytype: all_types.append(rel_primarytype.lower()) if 'secondary-type-list' in release['release-group']: - for secondarytype in release['release-group']['secondary-type-list']: - all_types.append(secondarytype.lower()) - if len(all_types) != 0: - all_types = ','.join(all_types) - info.albumtype = all_types.lower() + all_types.extend([secondarytype.lower() for secondarytype in\ + release['release-group']['secondary-type-list']]) + if 'type' in release['release-group']: + rel_type = release['release-group']['type'] + if rel_type and rel_type.lower() not in all_types: + all_types.append(rel_type) + if len(all_types): + info.albumtype = ', '.join(all_types).lower() # Release dates. release_date = release.get('date') From 0c61e08d7fa2e98cf03bacd631fc57bd1460f0be Mon Sep 17 00:00:00 2001 From: anshuman73 Date: Wed, 30 Nov 2016 20:34:40 +0530 Subject: [PATCH 04/71] Added new Album types as logs --- beets/autotag/mb.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index 452aa7d506..17cb62776c 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -278,20 +278,19 @@ def album_info(release): # Considers all release types (both primary and secondary) and # stores them as a comma-separated string - all_types = [] + if 'type' in release['release-group']: + reltype = release['release-group']['type'] + if reltype: + info.albumtype = reltype.lower() if 'primary-type' in release['release-group']: rel_primarytype = release['release-group']['primary-type'] if rel_primarytype: - all_types.append(rel_primarytype.lower()) + log.debug('Primary Type (new data): ' + rel_primarytype.lower()) if 'secondary-type-list' in release['release-group']: - all_types.extend([secondarytype.lower() for secondarytype in\ - release['release-group']['secondary-type-list']]) - if 'type' in release['release-group']: - rel_type = release['release-group']['type'] - if rel_type and rel_type.lower() not in all_types: - all_types.append(rel_type) - if len(all_types): - info.albumtype = ', '.join(all_types).lower() + if release['release-group']['secondary-type-list']: + log.debug('Secondary Type(s) (new data): ' + ', '.join( + [secondarytype.lower() for secondarytype in \ + release['release-group']['secondary-type-list']])) # Release dates. release_date = release.get('date') From 1217efd2bd4a63645a605136856cee30de25bec9 Mon Sep 17 00:00:00 2001 From: anshuman73 Date: Wed, 30 Nov 2016 20:44:12 +0530 Subject: [PATCH 05/71] Changed comment to reflect current status --- beets/autotag/mb.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index 17cb62776c..0d689917e8 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -276,8 +276,9 @@ def album_info(release): disambig.append(release.get('disambiguation')) info.albumdisambig = u', '.join(disambig) - # Considers all release types (both primary and secondary) and - # stores them as a comma-separated string + # Retrieves the Release type. + # Considers all other release types(including primary and secondary) (new) + # and logs them. if 'type' in release['release-group']: reltype = release['release-group']['type'] if reltype: From 082f944ac68d7b34f8ea3bcefb3a9cf5bacc15de Mon Sep 17 00:00:00 2001 From: anshuman73 Date: Thu, 1 Dec 2016 02:07:00 +0530 Subject: [PATCH 06/71] Removed redundant backslash --- beets/autotag/mb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index 0d689917e8..ed7287860d 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -290,7 +290,7 @@ def album_info(release): if 'secondary-type-list' in release['release-group']: if release['release-group']['secondary-type-list']: log.debug('Secondary Type(s) (new data): ' + ', '.join( - [secondarytype.lower() for secondarytype in \ + [secondarytype.lower() for secondarytype in release['release-group']['secondary-type-list']])) # Release dates. From 4745c262e238f5da7c28ee1328bf8af41e710a37 Mon Sep 17 00:00:00 2001 From: SpirosChadoulos Date: Thu, 13 Apr 2017 12:51:11 +0300 Subject: [PATCH 07/71] New export command --- beets/ui/commands.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 06ab6f0a5f..1afc7b540f 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -1450,7 +1450,7 @@ def modify_func(lib, opts, args): # move: Move/copy files to the library or a new base directory. -def move_items(lib, dest, query, copy, album, pretend, confirm=False): +def move_items(lib, dest, query, copy, album, pretend, confirm=False, export): """Moves or copies items to a new base directory, given by dest. If dest is None, then the library's base directory is used, making the command "consolidate" files. @@ -1479,17 +1479,20 @@ def move_items(lib, dest, query, copy, album, pretend, confirm=False): show_path_changes([(obj.path, obj.destination(basedir=dest)) for obj in objs]) else: - if confirm: - objs = ui.input_select_objects( - u'Really %s' % act, objs, - lambda o: show_path_changes( - [(o.path, o.destination(basedir=dest))])) + if export: + util.copy(item.path, item.destination(basedir=dest)) + else: + if confirm: + objs = ui.input_select_objects( + u'Really %s' % act, objs, + lambda o: show_path_changes( + [(o.path, o.destination(basedir=dest))])) - for obj in objs: - log.debug(u'moving: {0}', util.displayable_path(obj.path)) + for obj in objs: + log.debug(u'moving: {0}', util.displayable_path(obj.path)) - obj.move(copy, basedir=dest) - obj.store() + obj.move(copy, basedir=dest) + obj.store() def move_func(lib, opts, args): @@ -1500,7 +1503,7 @@ def move_func(lib, opts, args): raise ui.UserError(u'no such directory: %s' % dest) move_items(lib, dest, decargs(args), opts.copy, opts.album, opts.pretend, - opts.timid) + opts.timid, opts.export) move_cmd = ui.Subcommand( @@ -1522,6 +1525,10 @@ def move_func(lib, opts, args): u'-t', u'--timid', dest='timid', action='store_true', help=u'always confirm all actions' ) +move_cmd.parser.add_option( + u'-e', u'--export', default=False, action='store_true', + help=u'copy without changing the database path' +) move_cmd.parser.add_album_option() move_cmd.func = move_func default_commands.append(move_cmd) From 8f3ca12179acec8e855a31badeb3b55290086db6 Mon Sep 17 00:00:00 2001 From: SpirosChadoulos Date: Fri, 14 Apr 2017 12:19:16 +0300 Subject: [PATCH 08/71] Put export before confirm --- beets/ui/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 1afc7b540f..b748c12b2d 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -1450,7 +1450,7 @@ def modify_func(lib, opts, args): # move: Move/copy files to the library or a new base directory. -def move_items(lib, dest, query, copy, album, pretend, confirm=False, export): +def move_items(lib, dest, query, copy, album, pretend, export, confirm=False): """Moves or copies items to a new base directory, given by dest. If dest is None, then the library's base directory is used, making the command "consolidate" files. From d4413a2bc4e028034a2886d5cda8b432f7bcf0d5 Mon Sep 17 00:00:00 2001 From: SpirosChadoulos Date: Sat, 15 Apr 2017 00:54:09 +0300 Subject: [PATCH 09/71] obj instead of item --- beets/ui/commands.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index b748c12b2d..eec478ea69 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -1480,7 +1480,8 @@ def move_items(lib, dest, query, copy, album, pretend, export, confirm=False): for obj in objs]) else: if export: - util.copy(item.path, item.destination(basedir=dest)) + for obj in objs: + util.copy(obj.path, obj.destination(basedir=dest)) else: if confirm: objs = ui.input_select_objects( From a99b7e9e40864d1b7e5b2b6b4d7955e8e7683a28 Mon Sep 17 00:00:00 2001 From: SpirosChadoulos Date: Sun, 16 Apr 2017 15:53:11 +0300 Subject: [PATCH 10/71] Provided default value for export. --- beets/ui/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index eec478ea69..befbd1a45d 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -1450,7 +1450,7 @@ def modify_func(lib, opts, args): # move: Move/copy files to the library or a new base directory. -def move_items(lib, dest, query, copy, album, pretend, export, confirm=False): +def move_items(lib, dest, query, copy, album, pretend, export=False, confirm=False): """Moves or copies items to a new base directory, given by dest. If dest is None, then the library's base directory is used, making the command "consolidate" files. From 53618258faec970a0005b640d6811ad0aa04684a Mon Sep 17 00:00:00 2001 From: SpirosChadoulos Date: Wed, 19 Apr 2017 11:31:08 +0300 Subject: [PATCH 11/71] added 1 line before for loop --- beets/ui/commands.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index befbd1a45d..26ed4836b5 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -1480,6 +1480,10 @@ def move_items(lib, dest, query, copy, album, pretend, export=False, confirm=Fal for obj in objs]) else: if export: + objs = ui.input_select_objects( + u'Really %s' % act, objs, + lambda o: show_path_changes( + [(o.path, o.destination(basedir=dest))])) for obj in objs: util.copy(obj.path, obj.destination(basedir=dest)) else: From 70183070b2d622278e1d29e542f903ca1a8bd026 Mon Sep 17 00:00:00 2001 From: SpirosChadoulos Date: Wed, 19 Apr 2017 12:35:07 +0300 Subject: [PATCH 12/71] deleted else --- beets/ui/commands.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 06ab6f0a5f..88d9ccaf16 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -1450,7 +1450,7 @@ def modify_func(lib, opts, args): # move: Move/copy files to the library or a new base directory. -def move_items(lib, dest, query, copy, album, pretend, confirm=False): +def move_items(lib, dest, query, copy, album, pretend, export=False, confirm=False): """Moves or copies items to a new base directory, given by dest. If dest is None, then the library's base directory is used, making the command "consolidate" files. @@ -1479,6 +1479,9 @@ def move_items(lib, dest, query, copy, album, pretend, confirm=False): show_path_changes([(obj.path, obj.destination(basedir=dest)) for obj in objs]) else: + if export: + for obj in objs: + util.copy(obj.path, obj.destination(basedir=dest)) if confirm: objs = ui.input_select_objects( u'Really %s' % act, objs, @@ -1500,7 +1503,7 @@ def move_func(lib, opts, args): raise ui.UserError(u'no such directory: %s' % dest) move_items(lib, dest, decargs(args), opts.copy, opts.album, opts.pretend, - opts.timid) + opts.timid, opts.export) move_cmd = ui.Subcommand( @@ -1522,6 +1525,10 @@ def move_func(lib, opts, args): u'-t', u'--timid', dest='timid', action='store_true', help=u'always confirm all actions' ) +move_cmd.parser.add_option( + u'-e', u'--export', default=False, action='store_true', + help=u'copy without changing the database path' +) move_cmd.parser.add_album_option() move_cmd.func = move_func default_commands.append(move_cmd) From de57602e3451021a43bece47f1d45b5b7d15bd96 Mon Sep 17 00:00:00 2001 From: SpirosChadoulos Date: Wed, 19 Apr 2017 12:54:03 +0300 Subject: [PATCH 13/71] fixed certain errors --- beets/ui/commands.py | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 368dd1eb43..88d9ccaf16 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -1480,30 +1480,19 @@ def move_items(lib, dest, query, copy, album, pretend, export=False, confirm=Fal for obj in objs]) else: if export: -<<<<<<< HEAD for obj in objs: util.copy(obj.path, obj.destination(basedir=dest)) if confirm: -======= ->>>>>>> 53618258faec970a0005b640d6811ad0aa04684a objs = ui.input_select_objects( u'Really %s' % act, objs, lambda o: show_path_changes( [(o.path, o.destination(basedir=dest))])) - for obj in objs: - util.copy(obj.path, obj.destination(basedir=dest)) - else: - if confirm: - objs = ui.input_select_objects( - u'Really %s' % act, objs, - lambda o: show_path_changes( - [(o.path, o.destination(basedir=dest))])) - for obj in objs: - log.debug(u'moving: {0}', util.displayable_path(obj.path)) + for obj in objs: + log.debug(u'moving: {0}', util.displayable_path(obj.path)) - obj.move(copy, basedir=dest) - obj.store() + obj.move(copy, basedir=dest) + obj.store() def move_func(lib, opts, args): From 60318f1e02a248802a9ccbc2463c783111736ca3 Mon Sep 17 00:00:00 2001 From: SpirosChadoulos Date: Wed, 19 Apr 2017 13:07:04 +0300 Subject: [PATCH 14/71] fixed line length --- beets/ui/commands.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 88d9ccaf16..36876d2293 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -1450,7 +1450,8 @@ def modify_func(lib, opts, args): # move: Move/copy files to the library or a new base directory. -def move_items(lib, dest, query, copy, album, pretend, export=False, confirm=False): +def move_items(lib, dest, query, copy, album, pretend, export=False, + confirm=False): """Moves or copies items to a new base directory, given by dest. If dest is None, then the library's base directory is used, making the command "consolidate" files. From 61b832990f522af5581e43d2b9b83988228ef8a4 Mon Sep 17 00:00:00 2001 From: discopatrick Date: Tue, 25 Apr 2017 01:05:46 +0100 Subject: [PATCH 15/71] =?UTF-8?q?Add=20a=20date=20query=20precision=20of?= =?UTF-8?q?=20=E2=80=98hour=E2=80=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- beets/dbcore/query.py | 6 ++++-- test/test_datequery.py | 4 ++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index 8ce4dac41b..764ed04021 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -533,8 +533,8 @@ class Period(object): instants of time during January 2014. """ - precisions = ('year', 'month', 'day') - date_formats = ('%Y', '%Y-%m', '%Y-%m-%d') + precisions = ('year', 'month', 'day', 'hour') + date_formats = ('%Y', '%Y-%m', '%Y-%m-%d', '%Y-%m-%dT%H') def __init__(self, date, precision): """Create a period with the given date (a `datetime` object) and @@ -582,6 +582,8 @@ def open_right_endpoint(self): return date.replace(year=date.year + 1, month=1) elif 'day' == precision: return date + timedelta(days=1) + elif 'hour' == precision: + return date + timedelta(hours=1) else: raise ValueError(u'unhandled precision {0}'.format(precision)) diff --git a/test/test_datequery.py b/test/test_datequery.py index e81544aaad..d670d329ba 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -58,6 +58,10 @@ def test_month_precision_intervals(self): self.assertExcludes('1999-12..2000-02', '1999-11-30T23:59:59') self.assertExcludes('1999-12..2000-02', '2000-03-01T00:00:00') + def test_hour_precision_intervals(self): + self.assertContains('2000-01-01T12..2000-01-01T13', + '2000-01-01T12:30:00') + def test_unbounded_endpoints(self): self.assertContains('..', date=datetime.max) self.assertContains('..', date=datetime.min) From 5f2c47ec187eedb1d251e3b7f6b2d3a97c1f47f5 Mon Sep 17 00:00:00 2001 From: discopatrick Date: Tue, 25 Apr 2017 01:16:50 +0100 Subject: [PATCH 16/71] Test further hour precision intervals --- test/test_datequery.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/test_datequery.py b/test/test_datequery.py index d670d329ba..12ce224c49 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -59,8 +59,20 @@ def test_month_precision_intervals(self): self.assertExcludes('1999-12..2000-02', '2000-03-01T00:00:00') def test_hour_precision_intervals(self): + self.assertExcludes('2000-01-01T12..2000-01-01T13', + '2000-01-01T11:59:59') + self.assertContains('2000-01-01T12..2000-01-01T13', + '2000-01-01T12:00:00') self.assertContains('2000-01-01T12..2000-01-01T13', '2000-01-01T12:30:00') + self.assertContains('2000-01-01T12..2000-01-01T13', + '2000-01-01T13:30:00') + self.assertContains('2000-01-01T12..2000-01-01T13', + '2000-01-01T13:59:59') + self.assertExcludes('2000-01-01T12..2000-01-01T13', + '2000-01-01T14:00:00') + self.assertExcludes('2000-01-01T12..2000-01-01T13', + '2000-01-01T14:30:00') def test_unbounded_endpoints(self): self.assertContains('..', date=datetime.max) From ba324df0d1f17e4ffb7de97a580a0fe33610097b Mon Sep 17 00:00:00 2001 From: discopatrick Date: Tue, 25 Apr 2017 01:37:57 +0100 Subject: [PATCH 17/71] =?UTF-8?q?Add=20a=20date=20query=20precision=20of?= =?UTF-8?q?=20=E2=80=98minute=E2=80=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- beets/dbcore/query.py | 6 ++++-- test/test_datequery.py | 12 ++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index 764ed04021..da466a6ba0 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -533,8 +533,8 @@ class Period(object): instants of time during January 2014. """ - precisions = ('year', 'month', 'day', 'hour') - date_formats = ('%Y', '%Y-%m', '%Y-%m-%d', '%Y-%m-%dT%H') + precisions = ('year', 'month', 'day', 'hour', 'minute') + date_formats = ('%Y', '%Y-%m', '%Y-%m-%d', '%Y-%m-%dT%H', '%Y-%m-%dT%H:%M') def __init__(self, date, precision): """Create a period with the given date (a `datetime` object) and @@ -584,6 +584,8 @@ def open_right_endpoint(self): return date + timedelta(days=1) elif 'hour' == precision: return date + timedelta(hours=1) + elif 'minute' == precision: + return date + timedelta(minutes=1) else: raise ValueError(u'unhandled precision {0}'.format(precision)) diff --git a/test/test_datequery.py b/test/test_datequery.py index 12ce224c49..ea1974c411 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -74,6 +74,18 @@ def test_hour_precision_intervals(self): self.assertExcludes('2000-01-01T12..2000-01-01T13', '2000-01-01T14:30:00') + def test_minute_precision_intervals(self): + self.assertExcludes('2000-01-01T12:30..2000-01-01T12:31', + '2000-01-01T12:29:59') + self.assertContains('2000-01-01T12:30..2000-01-01T12:31', + '2000-01-01T12:30:00') + self.assertContains('2000-01-01T12:30..2000-01-01T12:31', + '2000-01-01T12:30:30') + self.assertContains('2000-01-01T12:30..2000-01-01T12:31', + '2000-01-01T12:30:59') + self.assertExcludes('2000-01-01T12:30..2000-01-01T12:31', + '2000-01-01T12:31:00') + def test_unbounded_endpoints(self): self.assertContains('..', date=datetime.max) self.assertContains('..', date=datetime.min) From b8e1c5675e21a6605c5770ae46f38057b8b54701 Mon Sep 17 00:00:00 2001 From: discopatrick Date: Tue, 25 Apr 2017 02:25:50 +0100 Subject: [PATCH 18/71] Fix tests --- test/test_datequery.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_datequery.py b/test/test_datequery.py index ea1974c411..b1398201c2 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -82,9 +82,9 @@ def test_minute_precision_intervals(self): self.assertContains('2000-01-01T12:30..2000-01-01T12:31', '2000-01-01T12:30:30') self.assertContains('2000-01-01T12:30..2000-01-01T12:31', - '2000-01-01T12:30:59') + '2000-01-01T12:31:59') self.assertExcludes('2000-01-01T12:30..2000-01-01T12:31', - '2000-01-01T12:31:00') + '2000-01-01T12:32:00') def test_unbounded_endpoints(self): self.assertContains('..', date=datetime.max) From 05f0072363ede939ca023ba6f1ad7ab2afde5105 Mon Sep 17 00:00:00 2001 From: discopatrick Date: Wed, 26 Apr 2017 23:46:17 +0100 Subject: [PATCH 19/71] Update docstring --- beets/dbcore/query.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index da466a6ba0..7b07284083 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -538,7 +538,8 @@ class Period(object): def __init__(self, date, precision): """Create a period with the given date (a `datetime` object) and - precision (a string, one of "year", "month", or "day"). + precision (a string, one of "year", "month", "day", "hour", "minute", or + "second"). """ if precision not in Period.precisions: raise ValueError(u'Invalid precision {0}'.format(precision)) From 6a71504545c154cc453b7f24f9b0328a140163fa Mon Sep 17 00:00:00 2001 From: discopatrick Date: Wed, 26 Apr 2017 23:53:51 +0100 Subject: [PATCH 20/71] Allow multiple date formats for each precision We want to allow datetime queries to be entered in multiple formats, e.g. with a 'T' or a space separator between date and time. This commit sets up that possibility, albeit with an additional dummy format for the time being. --- beets/dbcore/query.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index 7b07284083..3014e73a5c 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -534,7 +534,7 @@ class Period(object): """ precisions = ('year', 'month', 'day', 'hour', 'minute') - date_formats = ('%Y', '%Y-%m', '%Y-%m-%d', '%Y-%m-%dT%H', '%Y-%m-%dT%H:%M') + date_formats = (('%Y',), ('%Y-%m',), ('%Y-%m-%d',), ('%Y-%m-%dT%H', 'dummy-format'), ('%Y-%m-%dT%H:%M',)) def __init__(self, date, precision): """Create a period with the given date (a `datetime` object) and @@ -555,13 +555,19 @@ def parse(cls, string): if not string: return None date = None + found = False for ordinal, date_format in enumerate(cls.date_formats): - try: - date = datetime.strptime(string, date_format) + if found is True: + ordinal -= 1 break - except ValueError: - # Parsing failed. - pass + for format_option in date_format: + try: + date = datetime.strptime(string, format_option) + found = True + break + except ValueError: + # Parsing failed. + pass if date is None: raise InvalidQueryArgumentTypeError(string, 'a valid datetime string') From c3771f722ce08972cd73f7b34f96e075de98ad77 Mon Sep 17 00:00:00 2001 From: discopatrick Date: Thu, 27 Apr 2017 00:04:46 +0100 Subject: [PATCH 21/71] Allow hour precision queries to use space separator --- beets/dbcore/query.py | 2 +- test/test_datequery.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index 3014e73a5c..4c109c5b8e 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -534,7 +534,7 @@ class Period(object): """ precisions = ('year', 'month', 'day', 'hour', 'minute') - date_formats = (('%Y',), ('%Y-%m',), ('%Y-%m-%d',), ('%Y-%m-%dT%H', 'dummy-format'), ('%Y-%m-%dT%H:%M',)) + date_formats = (('%Y',), ('%Y-%m',), ('%Y-%m-%d',), ('%Y-%m-%dT%H', '%Y-%m-%d %H'), ('%Y-%m-%dT%H:%M',)) def __init__(self, date, precision): """Create a period with the given date (a `datetime` object) and diff --git a/test/test_datequery.py b/test/test_datequery.py index b1398201c2..743688b4d7 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -59,6 +59,7 @@ def test_month_precision_intervals(self): self.assertExcludes('1999-12..2000-02', '2000-03-01T00:00:00') def test_hour_precision_intervals(self): + # test with 'T' separator self.assertExcludes('2000-01-01T12..2000-01-01T13', '2000-01-01T11:59:59') self.assertContains('2000-01-01T12..2000-01-01T13', @@ -74,6 +75,12 @@ def test_hour_precision_intervals(self): self.assertExcludes('2000-01-01T12..2000-01-01T13', '2000-01-01T14:30:00') + # test with ' ' (space) separator + self.assertExcludes('2000-01-01 12..2000-01-01 13', + '2000-01-01T11:59:59') + self.assertContains('2000-01-01 12..2000-01-01 13', + '2000-01-01T12:00:00') + def test_minute_precision_intervals(self): self.assertExcludes('2000-01-01T12:30..2000-01-01T12:31', '2000-01-01T12:29:59') From 04e2975ee954e0117a7b79628809cc3ea0676ea0 Mon Sep 17 00:00:00 2001 From: discopatrick Date: Wed, 26 Apr 2017 23:21:47 +0100 Subject: [PATCH 22/71] Separate date formats onto individual lines --- beets/dbcore/query.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index 4c109c5b8e..949908bc25 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -534,7 +534,13 @@ class Period(object): """ precisions = ('year', 'month', 'day', 'hour', 'minute') - date_formats = (('%Y',), ('%Y-%m',), ('%Y-%m-%d',), ('%Y-%m-%dT%H', '%Y-%m-%d %H'), ('%Y-%m-%dT%H:%M',)) + date_formats = ( + ('%Y',), # year + ('%Y-%m',), # month + ('%Y-%m-%d',), # day + ('%Y-%m-%dT%H', '%Y-%m-%d %H'), # hour + ('%Y-%m-%dT%H:%M',) # minute + ) def __init__(self, date, precision): """Create a period with the given date (a `datetime` object) and From c10eb8f69dbde91d947b25d16c8596efee06d13a Mon Sep 17 00:00:00 2001 From: discopatrick Date: Wed, 26 Apr 2017 23:23:21 +0100 Subject: [PATCH 23/71] Keep docstring line <= 79 characters --- beets/dbcore/query.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index 949908bc25..e7001dcb20 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -544,8 +544,8 @@ class Period(object): def __init__(self, date, precision): """Create a period with the given date (a `datetime` object) and - precision (a string, one of "year", "month", "day", "hour", "minute", or - "second"). + precision (a string, one of "year", "month", "day", "hour", "minute", + or "second"). """ if precision not in Period.precisions: raise ValueError(u'Invalid precision {0}'.format(precision)) From 02bd19fb328a433165b25d29268f14b7d7f5996d Mon Sep 17 00:00:00 2001 From: discopatrick Date: Wed, 26 Apr 2017 23:33:53 +0100 Subject: [PATCH 24/71] Allow minute precision queries to use space separator --- beets/dbcore/query.py | 2 +- test/test_datequery.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index e7001dcb20..f9090e76bd 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -539,7 +539,7 @@ class Period(object): ('%Y-%m',), # month ('%Y-%m-%d',), # day ('%Y-%m-%dT%H', '%Y-%m-%d %H'), # hour - ('%Y-%m-%dT%H:%M',) # minute + ('%Y-%m-%dT%H:%M', '%Y-%m-%d %H:%M') # minute ) def __init__(self, date, precision): diff --git a/test/test_datequery.py b/test/test_datequery.py index 743688b4d7..14156febcc 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -93,6 +93,12 @@ def test_minute_precision_intervals(self): self.assertExcludes('2000-01-01T12:30..2000-01-01T12:31', '2000-01-01T12:32:00') + # test with ' ' (space) separator + self.assertExcludes('2000-01-01 12:30..2000-01-01 12:31', + '2000-01-01T12:29:59') + self.assertContains('2000-01-01 12:30..2000-01-01 12:31', + '2000-01-01T12:30:00') + def test_unbounded_endpoints(self): self.assertContains('..', date=datetime.max) self.assertContains('..', date=datetime.min) From 24890c77f1fe3228950fb92a943f6266c07ddaaa Mon Sep 17 00:00:00 2001 From: discopatrick Date: Thu, 27 Apr 2017 00:24:17 +0100 Subject: [PATCH 25/71] =?UTF-8?q?Add=20a=20date=20query=20precision=20of?= =?UTF-8?q?=20=E2=80=98second=E2=80=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- beets/dbcore/query.py | 7 +++++-- test/test_datequery.py | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index f9090e76bd..3e6ff270d2 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -533,13 +533,14 @@ class Period(object): instants of time during January 2014. """ - precisions = ('year', 'month', 'day', 'hour', 'minute') + precisions = ('year', 'month', 'day', 'hour', 'minute', 'second') date_formats = ( ('%Y',), # year ('%Y-%m',), # month ('%Y-%m-%d',), # day ('%Y-%m-%dT%H', '%Y-%m-%d %H'), # hour - ('%Y-%m-%dT%H:%M', '%Y-%m-%d %H:%M') # minute + ('%Y-%m-%dT%H:%M', '%Y-%m-%d %H:%M'), # minute + ('%Y-%m-%dT%H:%M:%S', '%Y-%m-%d %H:%M:%S') # second ) def __init__(self, date, precision): @@ -599,6 +600,8 @@ def open_right_endpoint(self): return date + timedelta(hours=1) elif 'minute' == precision: return date + timedelta(minutes=1) + elif 'second' == precision: + return date + timedelta(seconds=1) else: raise ValueError(u'unhandled precision {0}'.format(precision)) diff --git a/test/test_datequery.py b/test/test_datequery.py index 14156febcc..99381be193 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -99,6 +99,22 @@ def test_minute_precision_intervals(self): self.assertContains('2000-01-01 12:30..2000-01-01 12:31', '2000-01-01T12:30:00') + def test_second_precision_intervals(self): + self.assertExcludes('2000-01-01T12:30:50..2000-01-01T12:30:55', + '2000-01-01T12:30:49') + self.assertContains('2000-01-01T12:30:50..2000-01-01T12:30:55', + '2000-01-01T12:30:50') + self.assertContains('2000-01-01T12:30:50..2000-01-01T12:30:55', + '2000-01-01T12:30:55') + self.assertExcludes('2000-01-01T12:30:50..2000-01-01T12:30:55', + '2000-01-01T12:30:56') + + # test with ' ' (space) separator + self.assertExcludes('2000-01-01 12:30:50..2000-01-01 12:30:55', + '2000-01-01T12:30:49') + self.assertContains('2000-01-01 12:30:50..2000-01-01 12:30:55', + '2000-01-01T12:30:50') + def test_unbounded_endpoints(self): self.assertContains('..', date=datetime.max) self.assertContains('..', date=datetime.min) From 1ab913b200c8510e9f58a2b310819810b16df2c5 Mon Sep 17 00:00:00 2001 From: discopatrick Date: Thu, 27 Apr 2017 11:28:10 +0100 Subject: [PATCH 26/71] Test each valid datetime separator --- test/test_datequery.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/test_datequery.py b/test/test_datequery.py index 99381be193..c15b08cbdd 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -197,6 +197,21 @@ def test_invalid_date_query(self): with self.assertRaises(InvalidQueryArgumentTypeError): DateQuery('added', q) + def test_datetime_T_separator(self): + date_query = DateQuery('added', '2000-01-01T12') + self.assertEqual(date_query.interval.start, datetime(2000, 1, 1, 12)) + self.assertEqual(date_query.interval.end, datetime(2000, 1, 1, 13)) + + def test_datetime_t_separator(self): + date_query = DateQuery('added', '2000-01-01t12') + self.assertEqual(date_query.interval.start, datetime(2000, 1, 1, 12)) + self.assertEqual(date_query.interval.end, datetime(2000, 1, 1, 13)) + + def test_datetime_space_separator(self): + date_query = DateQuery('added', '2000-01-01 12') + self.assertEqual(date_query.interval.start, datetime(2000, 1, 1, 12)) + self.assertEqual(date_query.interval.end, datetime(2000, 1, 1, 13)) + def suite(): return unittest.TestLoader().loadTestsFromName(__name__) From 5a3b74f681b0278e11c1d3174f5dac18dcc7806e Mon Sep 17 00:00:00 2001 From: discopatrick Date: Thu, 27 Apr 2017 11:29:45 +0100 Subject: [PATCH 27/71] Test an invalid datetime separator raises error --- test/test_datequery.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/test_datequery.py b/test/test_datequery.py index c15b08cbdd..c702c70e57 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -212,6 +212,10 @@ def test_datetime_space_separator(self): self.assertEqual(date_query.interval.start, datetime(2000, 1, 1, 12)) self.assertEqual(date_query.interval.end, datetime(2000, 1, 1, 13)) + def test_datetime_invalid_separator(self): + with self.assertRaises(InvalidQueryArgumentTypeError): + DateQuery('added', '2000-01-01x12') + def suite(): return unittest.TestLoader().loadTestsFromName(__name__) From 6e6dd76513d11c90732d8156b72a9894e793f543 Mon Sep 17 00:00:00 2001 From: discopatrick Date: Thu, 27 Apr 2017 11:36:03 +0100 Subject: [PATCH 28/71] Remove space separator tests from test_x_precision_intervals tests This is not the correct place for space separator tests. Each test should test one thing only. Space separator tests are now in a separate test case. --- test/test_datequery.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/test/test_datequery.py b/test/test_datequery.py index c702c70e57..d2223a8d74 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -75,12 +75,6 @@ def test_hour_precision_intervals(self): self.assertExcludes('2000-01-01T12..2000-01-01T13', '2000-01-01T14:30:00') - # test with ' ' (space) separator - self.assertExcludes('2000-01-01 12..2000-01-01 13', - '2000-01-01T11:59:59') - self.assertContains('2000-01-01 12..2000-01-01 13', - '2000-01-01T12:00:00') - def test_minute_precision_intervals(self): self.assertExcludes('2000-01-01T12:30..2000-01-01T12:31', '2000-01-01T12:29:59') @@ -93,12 +87,6 @@ def test_minute_precision_intervals(self): self.assertExcludes('2000-01-01T12:30..2000-01-01T12:31', '2000-01-01T12:32:00') - # test with ' ' (space) separator - self.assertExcludes('2000-01-01 12:30..2000-01-01 12:31', - '2000-01-01T12:29:59') - self.assertContains('2000-01-01 12:30..2000-01-01 12:31', - '2000-01-01T12:30:00') - def test_second_precision_intervals(self): self.assertExcludes('2000-01-01T12:30:50..2000-01-01T12:30:55', '2000-01-01T12:30:49') @@ -109,12 +97,6 @@ def test_second_precision_intervals(self): self.assertExcludes('2000-01-01T12:30:50..2000-01-01T12:30:55', '2000-01-01T12:30:56') - # test with ' ' (space) separator - self.assertExcludes('2000-01-01 12:30:50..2000-01-01 12:30:55', - '2000-01-01T12:30:49') - self.assertContains('2000-01-01 12:30:50..2000-01-01 12:30:55', - '2000-01-01T12:30:50') - def test_unbounded_endpoints(self): self.assertContains('..', date=datetime.max) self.assertContains('..', date=datetime.min) From 50a2e37a4d8033e6340942f1d59322660eebcd59 Mon Sep 17 00:00:00 2001 From: discopatrick Date: Thu, 27 Apr 2017 15:58:08 +0100 Subject: [PATCH 29/71] Keep function names lowercase to pass flake8 tests --- test/test_datequery.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_datequery.py b/test/test_datequery.py index d2223a8d74..b8bb109257 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -179,12 +179,12 @@ def test_invalid_date_query(self): with self.assertRaises(InvalidQueryArgumentTypeError): DateQuery('added', q) - def test_datetime_T_separator(self): + def test_datetime_uppercase_t_separator(self): date_query = DateQuery('added', '2000-01-01T12') self.assertEqual(date_query.interval.start, datetime(2000, 1, 1, 12)) self.assertEqual(date_query.interval.end, datetime(2000, 1, 1, 13)) - def test_datetime_t_separator(self): + def test_datetime_lowercase_t_separator(self): date_query = DateQuery('added', '2000-01-01t12') self.assertEqual(date_query.interval.start, datetime(2000, 1, 1, 12)) self.assertEqual(date_query.interval.end, datetime(2000, 1, 1, 13)) From bab99f546f420d893581003fc5286ac35df70bdc Mon Sep 17 00:00:00 2001 From: SpirosChadoulos Date: Sun, 30 Apr 2017 15:02:50 +0300 Subject: [PATCH 30/71] Added a test for the new export feature --- test/test_ui.py | 77 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/test/test_ui.py b/test/test_ui.py index c519e66fb9..f31e677c35 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -476,6 +476,83 @@ def test_pretend_move_album(self): self.i.load() self.assertIn(b'srcfile', self.i.path) +class ExportTest(_common.TestCase): + def setUp(self): + super(MoveTest, self).setUp() + + self.io.install() + + self.libdir = os.path.join(self.temp_dir, b'testlibdir') + os.mkdir(self.libdir) + + self.itempath = os.path.join(self.libdir, b'srcfile') + shutil.copy(os.path.join(_common.RSRC, b'full.mp3'), self.itempath) + + # Add a file to the library but don't copy it in yet. + self.lib = library.Library(':memory:', self.libdir) + self.i = library.Item.from_path(self.itempath) + self.lib.add(self.i) + self.album = self.lib.add_album([self.i]) + + # Alternate destination directory. + self.otherdir = os.path.join(self.temp_dir, b'testotherdir') + + def _move(self, query=(), dest=None, copy=False, album=False, + pretend=False, export=True): + commands.move_items(self.lib, dest, query, copy, album, pretend, export) + + def test_move_item(self): + self._move() + self.i.load() + self.assertTrue(b'testlibdir' in self.i.path) + self.assertExists(self.i.path) + self.assertNotExists(self.itempath) + + def test_copy_item(self): + self._move(copy=True) + self.i.load() + self.assertTrue(b'testlibdir' in self.i.path) + self.assertExists(self.i.path) + self.assertExists(self.itempath) + + def test_move_album(self): + self._move(album=True) + self.i.load() + self.assertTrue(b'testlibdir' in self.i.path) + self.assertExists(self.i.path) + self.assertNotExists(self.itempath) + + def test_copy_album(self): + self._move(copy=True, album=True) + self.i.load() + self.assertTrue(b'testlibdir' in self.i.path) + self.assertExists(self.i.path) + self.assertExists(self.itempath) + + def test_move_item_custom_dir(self): + self._move(dest=self.otherdir) + self.i.load() + self.assertTrue(b'testotherdir' in self.i.path) + self.assertExists(self.i.path) + self.assertNotExists(self.itempath) + + def test_move_album_custom_dir(self): + self._move(dest=self.otherdir, album=True) + self.i.load() + self.assertTrue(b'testotherdir' in self.i.path) + self.assertExists(self.i.path) + self.assertNotExists(self.itempath) + + def test_pretend_move_item(self): + self._move(dest=self.otherdir, pretend=True) + self.i.load() + self.assertIn(b'srcfile', self.i.path) + + def test_pretend_move_album(self): + self._move(album=True, pretend=True) + self.i.load() + self.assertIn(b'srcfile', self.i.path) + class UpdateTest(_common.TestCase): def setUp(self): From a88192240e0d9ad37424cd74242ce25eb3d3b477 Mon Sep 17 00:00:00 2001 From: SpirosChadoulos Date: Sun, 30 Apr 2017 15:14:17 +0300 Subject: [PATCH 31/71] ExportTest correction --- test/test_ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_ui.py b/test/test_ui.py index f31e677c35..fd65ab0a64 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -478,7 +478,7 @@ def test_pretend_move_album(self): class ExportTest(_common.TestCase): def setUp(self): - super(MoveTest, self).setUp() + super(ExportTest, self).setUp() self.io.install() From 1c0b79590e53335ee7c82241b6e92d8ab9472f40 Mon Sep 17 00:00:00 2001 From: discopatrick Date: Mon, 1 May 2017 03:05:48 +0100 Subject: [PATCH 32/71] Refactor date-finding loop into an inner function This is an attempt to make this code more robust and more readable. --- beets/dbcore/query.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index 3e6ff270d2..89ee7aef7e 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -559,22 +559,21 @@ def parse(cls, string): string is empty, or raise an InvalidQueryArgumentTypeError if the string could not be parsed to a date. """ + + def find_date_and_format(string): + for ord, format in enumerate(cls.date_formats): + for format_option in format: + try: + date = datetime.strptime(string, format_option) + return date, ord + except ValueError: + # Parsing failed. + pass + return (None, None) + if not string: return None - date = None - found = False - for ordinal, date_format in enumerate(cls.date_formats): - if found is True: - ordinal -= 1 - break - for format_option in date_format: - try: - date = datetime.strptime(string, format_option) - found = True - break - except ValueError: - # Parsing failed. - pass + date, ordinal = find_date_and_format(string) if date is None: raise InvalidQueryArgumentTypeError(string, 'a valid datetime string') From 90c30d8564b7b31427f33c83b3933921329ec497 Mon Sep 17 00:00:00 2001 From: SpirosChadoulos Date: Thu, 4 May 2017 19:08:23 +0300 Subject: [PATCH 33/71] Added an if album: statement --- beets/ui/commands.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 36876d2293..840aa0a93a 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -1481,8 +1481,12 @@ def move_items(lib, dest, query, copy, album, pretend, export=False, for obj in objs]) else: if export: - for obj in objs: - util.copy(obj.path, obj.destination(basedir=dest)) + if album: + util.copy([(item.path, item.destination(basedir=dest)) + for obj in objs for item in obj.items()]) + else: + util.copy([(obj.path, obj.destination(basedir=dest)) + for obj in objs]) if confirm: objs = ui.input_select_objects( u'Really %s' % act, objs, From 167ae91b8d94fc252c9238c6a35f1083b09f2303 Mon Sep 17 00:00:00 2001 From: SpirosChadoulos Date: Fri, 5 May 2017 00:36:36 +0300 Subject: [PATCH 34/71] Changes at line 1486 --- beets/ui/commands.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 840aa0a93a..7e2095dd1f 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -1482,11 +1482,12 @@ def move_items(lib, dest, query, copy, album, pretend, export=False, else: if export: if album: - util.copy([(item.path, item.destination(basedir=dest)) - for obj in objs for item in obj.items()]) + for obj in objs: + for item in obj.items(): + util.copy(item.path, item.destination(basedir=dest)) else: - util.copy([(obj.path, obj.destination(basedir=dest)) - for obj in objs]) + for item in objs: + util.copy(item.path, item.destination(basedir=dest)) if confirm: objs = ui.input_select_objects( u'Really %s' % act, objs, From f5b23fffd4494d1d799eaaef7fad4e4c0b926d50 Mon Sep 17 00:00:00 2001 From: SpirosChadoulos Date: Sat, 6 May 2017 15:03:42 +0300 Subject: [PATCH 35/71] Replaced all AssertNotExists with AssertExists --- test/test_ui.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/test_ui.py b/test/test_ui.py index fd65ab0a64..0f5ad5d678 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -506,7 +506,7 @@ def test_move_item(self): self.i.load() self.assertTrue(b'testlibdir' in self.i.path) self.assertExists(self.i.path) - self.assertNotExists(self.itempath) + self.assertExists(self.itempath) def test_copy_item(self): self._move(copy=True) @@ -520,7 +520,7 @@ def test_move_album(self): self.i.load() self.assertTrue(b'testlibdir' in self.i.path) self.assertExists(self.i.path) - self.assertNotExists(self.itempath) + self.assertExists(self.itempath) def test_copy_album(self): self._move(copy=True, album=True) @@ -534,14 +534,14 @@ def test_move_item_custom_dir(self): self.i.load() self.assertTrue(b'testotherdir' in self.i.path) self.assertExists(self.i.path) - self.assertNotExists(self.itempath) + self.assertExists(self.itempath) def test_move_album_custom_dir(self): self._move(dest=self.otherdir, album=True) self.i.load() self.assertTrue(b'testotherdir' in self.i.path) self.assertExists(self.i.path) - self.assertNotExists(self.itempath) + self.assertExists(self.itempath) def test_pretend_move_item(self): self._move(dest=self.otherdir, pretend=True) From 0dc948d9d34cb81e63543767a088f41b50182899 Mon Sep 17 00:00:00 2001 From: SpirosChadoulos Date: Thu, 11 May 2017 12:11:21 +0300 Subject: [PATCH 36/71] Made sure that the destination directory will exist --- beets/ui/commands.py | 9 +++++++-- test/test_ui.py | 2 ++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 7e2095dd1f..db34ed8720 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -1481,13 +1481,18 @@ def move_items(lib, dest, query, copy, album, pretend, export=False, for obj in objs]) else: if export: + if album: for obj in objs: for item in obj.items(): + # Create necessary ancestry for the copy. + util.mkdirall(item.destination(basedir=dest)) util.copy(item.path, item.destination(basedir=dest)) else: - for item in objs: - util.copy(item.path, item.destination(basedir=dest)) + for obj in objs: + # Create necessary ancestry for the copy. + util.mkdirall(obj.destination(basedir=dest)) + util.copy(obj.path, obj.destination(basedir=dest)) if confirm: objs = ui.input_select_objects( u'Really %s' % act, objs, diff --git a/test/test_ui.py b/test/test_ui.py index 0f5ad5d678..33266d379d 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -543,6 +543,8 @@ def test_move_album_custom_dir(self): self.assertExists(self.i.path) self.assertExists(self.itempath) + + def test_pretend_move_item(self): self._move(dest=self.otherdir, pretend=True) self.i.load() From 51835e762ffe10b279d7a49e459327acdc502630 Mon Sep 17 00:00:00 2001 From: SpirosChadoulos Date: Thu, 11 May 2017 12:30:21 +0300 Subject: [PATCH 37/71] Minor fixes to move tests --- test/test_ui.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/test_ui.py b/test/test_ui.py index 33266d379d..f017d82109 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -506,7 +506,7 @@ def test_move_item(self): self.i.load() self.assertTrue(b'testlibdir' in self.i.path) self.assertExists(self.i.path) - self.assertExists(self.itempath) + self.assertNotExists(self.itempath) def test_copy_item(self): self._move(copy=True) @@ -520,7 +520,7 @@ def test_move_album(self): self.i.load() self.assertTrue(b'testlibdir' in self.i.path) self.assertExists(self.i.path) - self.assertExists(self.itempath) + self.assertNotExists(self.itempath) def test_copy_album(self): self._move(copy=True, album=True) @@ -534,14 +534,14 @@ def test_move_item_custom_dir(self): self.i.load() self.assertTrue(b'testotherdir' in self.i.path) self.assertExists(self.i.path) - self.assertExists(self.itempath) + self.assertNotExists(self.itempath) def test_move_album_custom_dir(self): self._move(dest=self.otherdir, album=True) self.i.load() self.assertTrue(b'testotherdir' in self.i.path) self.assertExists(self.i.path) - self.assertExists(self.itempath) + self.assertNotExists(self.itempath) From c4ef23d9f716d889206391bdd3811c59467f1192 Mon Sep 17 00:00:00 2001 From: SpirosChadoulos Date: Thu, 11 May 2017 12:45:03 +0300 Subject: [PATCH 38/71] Minor Flake fixes --- test/test_ui.py | 72 ++++++++++++++++++++++++------------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/test/test_ui.py b/test/test_ui.py index f017d82109..92a4d4ddb4 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -156,7 +156,6 @@ def test_remove_items_with_force_delete(self): class ModifyTest(unittest.TestCase, TestHelper): - def setUp(self): self.setup_beets() self.album = self.add_album_fixture() @@ -347,7 +346,6 @@ def test_arg_parsing_equals_in_value(self): class WriteTest(unittest.TestCase, TestHelper): - def setUp(self): self.setup_beets() @@ -476,6 +474,7 @@ def test_pretend_move_album(self): self.i.load() self.assertIn(b'srcfile', self.i.path) + class ExportTest(_common.TestCase): def setUp(self): super(ExportTest, self).setUp() @@ -499,7 +498,8 @@ def setUp(self): def _move(self, query=(), dest=None, copy=False, album=False, pretend=False, export=True): - commands.move_items(self.lib, dest, query, copy, album, pretend, export) + commands.move_items(self.lib, dest, query, copy, album, + pretend, export) def test_move_item(self): self._move() @@ -543,8 +543,6 @@ def test_move_album_custom_dir(self): self.assertExists(self.i.path) self.assertNotExists(self.itempath) - - def test_pretend_move_item(self): self._move(dest=self.otherdir, pretend=True) self.i.load() @@ -901,37 +899,37 @@ def test_cli_config_file_overwrites_beetsdir_defaults(self): self.run_command('--config', cli_config_path, 'test', lib=None) self.assertEqual(config['anoption'].get(), 'cli overwrite') -# @unittest.skip('Difficult to implement with optparse') -# def test_multiple_cli_config_files(self): -# cli_config_path_1 = os.path.join(self.temp_dir, b'config.yaml') -# cli_config_path_2 = os.path.join(self.temp_dir, b'config_2.yaml') -# -# with open(cli_config_path_1, 'w') as file: -# file.write('first: value') -# -# with open(cli_config_path_2, 'w') as file: -# file.write('second: value') -# -# self.run_command('--config', cli_config_path_1, -# '--config', cli_config_path_2, 'test', lib=None) -# self.assertEqual(config['first'].get(), 'value') -# self.assertEqual(config['second'].get(), 'value') -# -# @unittest.skip('Difficult to implement with optparse') -# def test_multiple_cli_config_overwrite(self): -# cli_config_path = os.path.join(self.temp_dir, b'config.yaml') -# cli_overwrite_config_path = os.path.join(self.temp_dir, -# b'overwrite_config.yaml') -# -# with open(cli_config_path, 'w') as file: -# file.write('anoption: value') -# -# with open(cli_overwrite_config_path, 'w') as file: -# file.write('anoption: overwrite') -# -# self.run_command('--config', cli_config_path, -# '--config', cli_overwrite_config_path, 'test') -# self.assertEqual(config['anoption'].get(), 'cli overwrite') + # @unittest.skip('Difficult to implement with optparse') + # def test_multiple_cli_config_files(self): + # cli_config_path_1 = os.path.join(self.temp_dir, b'config.yaml') + # cli_config_path_2 = os.path.join(self.temp_dir, b'config_2.yaml') + # + # with open(cli_config_path_1, 'w') as file: + # file.write('first: value') + # + # with open(cli_config_path_2, 'w') as file: + # file.write('second: value') + # + # self.run_command('--config', cli_config_path_1, + # '--config', cli_config_path_2, 'test', lib=None) + # self.assertEqual(config['first'].get(), 'value') + # self.assertEqual(config['second'].get(), 'value') + # + # @unittest.skip('Difficult to implement with optparse') + # def test_multiple_cli_config_overwrite(self): + # cli_config_path = os.path.join(self.temp_dir, b'config.yaml') + # cli_overwrite_config_path = os.path.join(self.temp_dir, + # b'overwrite_config.yaml') + # + # with open(cli_config_path, 'w') as file: + # file.write('anoption: value') + # + # with open(cli_overwrite_config_path, 'w') as file: + # file.write('anoption: overwrite') + # + # self.run_command('--config', cli_config_path, + # '--config', cli_overwrite_config_path, 'test') + # self.assertEqual(config['anoption'].get(), 'cli overwrite') def test_cli_config_paths_resolve_relative_to_user_dir(self): cli_config_path = os.path.join(self.temp_dir, b'config.yaml') @@ -1271,6 +1269,7 @@ class CommonOptionsParserCliTest(unittest.TestCase, TestHelper): """Test CommonOptionsParser and formatting LibModel formatting on 'list' command. """ + def setUp(self): self.setup_beets() self.lib = library.Library(':memory:') @@ -1467,5 +1466,6 @@ def in_encoding_default_utf8(self): def suite(): return unittest.TestLoader().loadTestsFromName(__name__) + if __name__ == '__main__': unittest.main(defaultTest='suite') From 8f62e8bc684a9f943baa1db4eaac1b5b43a09ec2 Mon Sep 17 00:00:00 2001 From: SpirosChadoulos Date: Fri, 12 May 2017 14:49:03 +0300 Subject: [PATCH 39/71] Requested changes done --- beets/ui/commands.py | 16 ++++---- test/test_ui.py | 93 +++++++++++++++++--------------------------- 2 files changed, 45 insertions(+), 64 deletions(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index db34ed8720..b7792d0cf8 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -1447,6 +1447,12 @@ def modify_func(lib, opts, args): modify_cmd.func = modify_func default_commands.append(modify_cmd) +#export(file) function copies a file without modifying the database. + +def export(file): + # Create necessary ancestry for the copy. + util.mkdirall(file.destination(basedir=dest)) + util.copy(file.path, file.destination(basedir=dest)) # move: Move/copy files to the library or a new base directory. @@ -1480,19 +1486,15 @@ def move_items(lib, dest, query, copy, album, pretend, export=False, show_path_changes([(obj.path, obj.destination(basedir=dest)) for obj in objs]) else: + #Copying files without modifying the database. if export: - if album: for obj in objs: for item in obj.items(): - # Create necessary ancestry for the copy. - util.mkdirall(item.destination(basedir=dest)) - util.copy(item.path, item.destination(basedir=dest)) + export(item) else: for obj in objs: - # Create necessary ancestry for the copy. - util.mkdirall(obj.destination(basedir=dest)) - util.copy(obj.path, obj.destination(basedir=dest)) + export(obj) if confirm: objs = ui.input_select_objects( u'Really %s' % act, objs, diff --git a/test/test_ui.py b/test/test_ui.py index 92a4d4ddb4..f34cf72cb7 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -501,54 +501,33 @@ def _move(self, query=(), dest=None, copy=False, album=False, commands.move_items(self.lib, dest, query, copy, album, pretend, export) - def test_move_item(self): + def test_export_item(self): self._move() self.i.load() self.assertTrue(b'testlibdir' in self.i.path) self.assertExists(self.i.path) self.assertNotExists(self.itempath) - def test_copy_item(self): - self._move(copy=True) - self.i.load() - self.assertTrue(b'testlibdir' in self.i.path) - self.assertExists(self.i.path) - self.assertExists(self.itempath) - - def test_move_album(self): - self._move(album=True) - self.i.load() - self.assertTrue(b'testlibdir' in self.i.path) - self.assertExists(self.i.path) - self.assertNotExists(self.itempath) - - def test_copy_album(self): - self._move(copy=True, album=True) - self.i.load() - self.assertTrue(b'testlibdir' in self.i.path) - self.assertExists(self.i.path) - self.assertExists(self.itempath) - - def test_move_item_custom_dir(self): + def test_export_item_custom_dir(self): self._move(dest=self.otherdir) self.i.load() self.assertTrue(b'testotherdir' in self.i.path) self.assertExists(self.i.path) self.assertNotExists(self.itempath) - def test_move_album_custom_dir(self): + def test_export_album_custom_dir(self): self._move(dest=self.otherdir, album=True) self.i.load() self.assertTrue(b'testotherdir' in self.i.path) self.assertExists(self.i.path) self.assertNotExists(self.itempath) - def test_pretend_move_item(self): + def test_pretend_export_item(self): self._move(dest=self.otherdir, pretend=True) self.i.load() self.assertIn(b'srcfile', self.i.path) - def test_pretend_move_album(self): + def test_pretend_export_album(self): self._move(album=True, pretend=True) self.i.load() self.assertIn(b'srcfile', self.i.path) @@ -899,37 +878,37 @@ def test_cli_config_file_overwrites_beetsdir_defaults(self): self.run_command('--config', cli_config_path, 'test', lib=None) self.assertEqual(config['anoption'].get(), 'cli overwrite') - # @unittest.skip('Difficult to implement with optparse') - # def test_multiple_cli_config_files(self): - # cli_config_path_1 = os.path.join(self.temp_dir, b'config.yaml') - # cli_config_path_2 = os.path.join(self.temp_dir, b'config_2.yaml') - # - # with open(cli_config_path_1, 'w') as file: - # file.write('first: value') - # - # with open(cli_config_path_2, 'w') as file: - # file.write('second: value') - # - # self.run_command('--config', cli_config_path_1, - # '--config', cli_config_path_2, 'test', lib=None) - # self.assertEqual(config['first'].get(), 'value') - # self.assertEqual(config['second'].get(), 'value') - # - # @unittest.skip('Difficult to implement with optparse') - # def test_multiple_cli_config_overwrite(self): - # cli_config_path = os.path.join(self.temp_dir, b'config.yaml') - # cli_overwrite_config_path = os.path.join(self.temp_dir, - # b'overwrite_config.yaml') - # - # with open(cli_config_path, 'w') as file: - # file.write('anoption: value') - # - # with open(cli_overwrite_config_path, 'w') as file: - # file.write('anoption: overwrite') - # - # self.run_command('--config', cli_config_path, - # '--config', cli_overwrite_config_path, 'test') - # self.assertEqual(config['anoption'].get(), 'cli overwrite') +# @unittest.skip('Difficult to implement with optparse') +# def test_multiple_cli_config_files(self): +# cli_config_path_1 = os.path.join(self.temp_dir, b'config.yaml') +# cli_config_path_2 = os.path.join(self.temp_dir, b'config_2.yaml') +# +# with open(cli_config_path_1, 'w') as file: +# file.write('first: value') +# +# with open(cli_config_path_2, 'w') as file: +# file.write('second: value') +# +# self.run_command('--config', cli_config_path_1, +# '--config', cli_config_path_2, 'test', lib=None) +# self.assertEqual(config['first'].get(), 'value') +# self.assertEqual(config['second'].get(), 'value') +# +# @unittest.skip('Difficult to implement with optparse') +# def test_multiple_cli_config_overwrite(self): +# cli_config_path = os.path.join(self.temp_dir, b'config.yaml') +# cli_overwrite_config_path = os.path.join(self.temp_dir, +# b'overwrite_config.yaml') +# +# with open(cli_config_path, 'w') as file: +# file.write('anoption: value') +# +# with open(cli_overwrite_config_path, 'w') as file: +# file.write('anoption: overwrite') +# +# self.run_command('--config', cli_config_path, +# '--config', cli_overwrite_config_path, 'test') +# self.assertEqual(config['anoption'].get(), 'cli overwrite') def test_cli_config_paths_resolve_relative_to_user_dir(self): cli_config_path = os.path.join(self.temp_dir, b'config.yaml') From 169cf596b0f98edcddcc7a9fda791721d3c37f45 Mon Sep 17 00:00:00 2001 From: SpirosChadoulos Date: Fri, 12 May 2017 15:08:06 +0300 Subject: [PATCH 40/71] Fixed bool variable error --- beets/ui/commands.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index b7792d0cf8..64f3229d5f 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -1447,13 +1447,6 @@ def modify_func(lib, opts, args): modify_cmd.func = modify_func default_commands.append(modify_cmd) -#export(file) function copies a file without modifying the database. - -def export(file): - # Create necessary ancestry for the copy. - util.mkdirall(file.destination(basedir=dest)) - util.copy(file.path, file.destination(basedir=dest)) - # move: Move/copy files to the library or a new base directory. def move_items(lib, dest, query, copy, album, pretend, export=False, @@ -1488,6 +1481,12 @@ def move_items(lib, dest, query, copy, album, pretend, export=False, else: #Copying files without modifying the database. if export: + # export(file) function copies a file without modifying the database. + + def export(file): + # Create necessary ancestry for the copy. + util.mkdirall(file.destination(basedir=dest)) + util.copy(file.path, file.destination(basedir=dest)) if album: for obj in objs: for item in obj.items(): From 7c9198934488c6b5b9ee7e11a3e1275d901c3a8a Mon Sep 17 00:00:00 2001 From: SpirosChadoulos Date: Fri, 12 May 2017 15:25:36 +0300 Subject: [PATCH 41/71] Minor flake fixes --- beets/ui/commands.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 64f3229d5f..7ffa90b11e 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -1447,6 +1447,7 @@ def modify_func(lib, opts, args): modify_cmd.func = modify_func default_commands.append(modify_cmd) + # move: Move/copy files to the library or a new base directory. def move_items(lib, dest, query, copy, album, pretend, export=False, @@ -1479,9 +1480,10 @@ def move_items(lib, dest, query, copy, album, pretend, export=False, show_path_changes([(obj.path, obj.destination(basedir=dest)) for obj in objs]) else: - #Copying files without modifying the database. + # Copying files without modifying the database. if export: - # export(file) function copies a file without modifying the database. + # export(file) function copies a file without modifying + # the database. def export(file): # Create necessary ancestry for the copy. @@ -1490,7 +1492,7 @@ def export(file): if album: for obj in objs: for item in obj.items(): - export(item) + export(item) else: for obj in objs: export(obj) From 3c852d3539aafa2c9ad76a72ac88416379110139 Mon Sep 17 00:00:00 2001 From: SpirosChadoulos Date: Sat, 13 May 2017 12:26:31 +0300 Subject: [PATCH 42/71] docs --- docs/reference/cli.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/reference/cli.rst b/docs/reference/cli.rst index 403c1e1747..7baa25f05b 100644 --- a/docs/reference/cli.rst +++ b/docs/reference/cli.rst @@ -72,7 +72,7 @@ box. To extract `rar` files, install the `rarfile`_ package and the Optional command flags: * By default, the command copies files your the library directory and - updates the ID3 tags on your music. In order to move the files, instead of + updates the ID3 tags on your music. In order to move the files, instead of copying, use the ``-m`` (move) option. If you'd like to leave your music files untouched, try the ``-C`` (don't copy) and ``-W`` (don't write tags) options. You can also disable this behavior by default in the @@ -275,6 +275,7 @@ query are renamed into your library directory structure. By specifying a destination directory with ``-d`` manually, you can move items matching a query anywhere in your filesystem. The ``-c`` option copies files instead of moving them. As with other commands, the ``-a`` option matches albums instead of items. +The ``-e`` flag (for "export") copies files without changing the database. To perform a "dry run", just use the ``-p`` (for "pretend") flag. This will show you a list of files that would be moved but won't actually change anything From 9fe171c5cedd1a2abd96679d89ffc159239b1985 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Koutensk=C3=BD?= Date: Wed, 17 May 2017 21:37:38 +0200 Subject: [PATCH 43/71] properly safe cast unicode as int --- beets/mediafile.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/beets/mediafile.py b/beets/mediafile.py index 9242ab1f19..50c2cb76bb 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -154,10 +154,12 @@ def _safe_cast(out_type, val): return int(val) else: # Process any other type as a string. - if not isinstance(val, six.string_types): + if isinstance(val, bytes): + val = val.decode('utf-8', 'ignore') + elif not isinstance(val, six.string_types): val = six.text_type(val) # Get a number from the front of the string. - val = re.match(r'[0-9]*', val.strip()).group(0) + val = re.match(r'[\+-]?[0-9]*', val.strip()).group(0) if not val: return 0 else: From ddfe44266b3e2588a11884f1d3b3ba530c3800d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Koutensk=C3=BD?= Date: Wed, 17 May 2017 21:41:12 +0200 Subject: [PATCH 44/71] r128 gain tags in mediafile test --- test/test_mediafile.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/test_mediafile.py b/test/test_mediafile.py index 18dcc11a38..5a004b7e88 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -374,6 +374,8 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin, 'rg_track_gain', 'rg_album_peak', 'rg_album_gain', + 'r128_track_gain', + 'r128_album_gain', 'albumartist', 'mb_albumartistid', 'artist_sort', @@ -672,6 +674,9 @@ def _generate_tags(self, base=None): if key.startswith('rg_'): # ReplayGain is float tags[key] = 1.0 + elif key.startswith('r128_'): + # R128 is int + tags[key] = -1 else: tags[key] = 'value\u2010%s' % key From 2685f133152c6b059ab116560c5ee357c6550404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Koutensk=C3=BD?= Date: Sat, 13 May 2017 19:50:43 +0200 Subject: [PATCH 45/71] replaygain: support r128 --- beets/library.py | 4 ++ beets/mediafile.py | 32 +++++++++++++++ beetsplug/replaygain.py | 86 +++++++++++++++++++++++++++++++++++++---- 3 files changed, 114 insertions(+), 8 deletions(-) diff --git a/beets/library.py b/beets/library.py index 4b2b194fc7..991de59e83 100644 --- a/beets/library.py +++ b/beets/library.py @@ -454,6 +454,8 @@ class Item(LibModel): 'rg_track_peak': types.NULL_FLOAT, 'rg_album_gain': types.NULL_FLOAT, 'rg_album_peak': types.NULL_FLOAT, + 'r128_track_gain': types.PaddedInt(6), + 'r128_album_gain': types.PaddedInt(6), 'original_year': types.PaddedInt(4), 'original_month': types.PaddedInt(2), 'original_day': types.PaddedInt(2), @@ -898,6 +900,7 @@ class Album(LibModel): 'albumdisambig': types.STRING, 'rg_album_gain': types.NULL_FLOAT, 'rg_album_peak': types.NULL_FLOAT, + 'r128_album_gain': types.PaddedInt(6), 'original_year': types.PaddedInt(4), 'original_month': types.PaddedInt(2), 'original_day': types.PaddedInt(2), @@ -941,6 +944,7 @@ class Album(LibModel): 'albumdisambig', 'rg_album_gain', 'rg_album_peak', + 'r128_album_gain', 'original_year', 'original_month', 'original_day', diff --git a/beets/mediafile.py b/beets/mediafile.py index 50c2cb76bb..d928e05c4a 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -2007,6 +2007,38 @@ def update(self, dict): out_type=float, ) + # EBU R128 fields. + r128_track_gain = MediaField( + MP3DescStorageStyle( + u'R128_TRACK_GAIN' + ), + MP4StorageStyle( + '----:com.apple.iTunes:R128_TRACK_GAIN' + ), + StorageStyle( + u'R128_TRACK_GAIN' + ), + ASFStorageStyle( + u'R128_TRACK_GAIN' + ), + out_type=int, + ) + r128_album_gain = MediaField( + MP3DescStorageStyle( + u'R128_ALBUM_GAIN' + ), + MP4StorageStyle( + '----:com.apple.iTunes:R128_ALBUM_GAIN' + ), + StorageStyle( + u'R128_ALBUM_GAIN' + ), + ASFStorageStyle( + u'R128_ALBUM_GAIN' + ), + out_type=int, + ) + initial_key = MediaField( MP3StorageStyle('TKEY'), MP4StorageStyle('----:com.apple.iTunes:initialkey'), diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index 4cf7da7c50..69fa8d6c1d 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -805,6 +805,7 @@ def __init__(self): 'auto': True, 'backend': u'command', 'targetlevel': 89, + 'r128': ['Opus'], }) self.overwrite = self.config['overwrite'].get(bool) @@ -822,6 +823,9 @@ def __init__(self): if self.config['auto']: self.import_stages = [self.imported] + # Formats to use R128. + self.r128_whitelist = self.config['r128'].as_str_seq() + try: self.backend_instance = self.backends[backend_name]( self.config, self._log @@ -830,9 +834,19 @@ def __init__(self): raise ui.UserError( u'replaygain initialization failed: {0}'.format(e)) + self.r128_backend_instance = '' + + def should_use_r128(self, item): + """Checks the plugin setting to decide whether the calculation + should be done using the EBU R128 standard and use R128_ tags instead. + """ + return item.format in self.r128_whitelist + def track_requires_gain(self, item): return self.overwrite or \ - (not item.rg_track_gain or not item.rg_track_peak) + (self.should_use_r128(item) and not item.r128_track_gain) or \ + (not self.should_use_r128(item) and + (not item.rg_track_gain or not item.rg_track_peak)) def album_requires_gain(self, album): # Skip calculating gain only when *all* files don't need @@ -840,8 +854,12 @@ def album_requires_gain(self, album): # needs recalculation, we still get an accurate album gain # value. return self.overwrite or \ - any([not item.rg_album_gain or not item.rg_album_peak - for item in album.items()]) + any([self.should_use_r128(item) and + (not item.r128_item_gain or not item.r128_album_gain) + for item in album.items()]) or \ + any([not self.should_use_r128(item) and + (not item.rg_album_gain or not item.rg_album_peak) + for item in album.items()]) def store_track_gain(self, item, track_gain): item.rg_track_gain = track_gain.gain @@ -851,6 +869,12 @@ def store_track_gain(self, item, track_gain): self._log.debug(u'applied track gain {0}, peak {1}', item.rg_track_gain, item.rg_track_peak) + def store_track_r128_gain(self, item, track_gain): + item.r128_track_gain = int(round(track_gain.gain * pow(2, 8))) + item.store() + + self._log.debug(u'applied track gain {0}', item.r128_track_gain) + def store_album_gain(self, album, album_gain): album.rg_album_gain = album_gain.gain album.rg_album_peak = album_gain.peak @@ -859,6 +883,12 @@ def store_album_gain(self, album, album_gain): self._log.debug(u'applied album gain {0}, peak {1}', album.rg_album_gain, album.rg_album_peak) + def store_album_r128_gain(self, album, album_gain): + album.r128_album_gain = int(round(album_gain.gain * pow(2, 8))) + album.store() + + self._log.debug(u'applied album gain {0}', album.r128_album_gain) + def handle_album(self, album, write): """Compute album and track replay gain store it in all of the album's items. @@ -873,17 +903,35 @@ def handle_album(self, album, write): self._log.info(u'analyzing {0}', album) + if (any([self.should_use_r128(item) for item in album.items()]) and + all(([self.should_use_r128(item) for item in album.items()]))): + raise ReplayGainError( + u"Mix of ReplayGain and EBU R128 detected" + u"for some tracks in album {0}".format(album) + ) + + if any([self.should_use_r128(item) for item in album.items()]): + if self.r128_backend_instance == '': + self.init_r128_backend() + backend_instance = self.r128_backend_instance + store_track_gain = self.store_track_r128_gain + store_album_gain = self.store_album_r128_gain + else: + backend_instance = self.backend_instance + store_track_gain = self.store_track_gain + store_album_gain = self.store_album_gain + try: - album_gain = self.backend_instance.compute_album_gain(album) + album_gain = backend_instance.compute_album_gain(album) if len(album_gain.track_gains) != len(album.items()): raise ReplayGainError( u"ReplayGain backend failed " u"for some tracks in album {0}".format(album) ) - self.store_album_gain(album, album_gain.album_gain) + store_album_gain(album, album_gain.album_gain) for item, track_gain in zip(album.items(), album_gain.track_gains): - self.store_track_gain(item, track_gain) + store_track_gain(item, track_gain) if write: item.try_write() except ReplayGainError as e: @@ -905,14 +953,23 @@ def handle_track(self, item, write): self._log.info(u'analyzing {0}', item) + if self.should_use_r128(item): + if self.r128_backend_instance == '': + self.init_r128_backend() + backend_instance = self.r128_backend_instance + store_track_gain = self.store_track_r128_gain + else: + backend_instance = self.backend_instance + store_track_gain = self.store_track_gain + try: - track_gains = self.backend_instance.compute_track_gain([item]) + track_gains = backend_instance.compute_track_gain([item]) if len(track_gains) != 1: raise ReplayGainError( u"ReplayGain backend failed for track {0}".format(item) ) - self.store_track_gain(item, track_gains[0]) + store_track_gain(item, track_gains[0]) if write: item.try_write() except ReplayGainError as e: @@ -921,6 +978,19 @@ def handle_track(self, item, write): raise ui.UserError( u"Fatal replay gain error: {0}".format(e)) + def init_r128_backend(self): + backend_name = 'bs1770gain' + + try: + self.r128_backend_instance = self.backends[backend_name]( + self.config, self._log + ) + except (ReplayGainError, FatalReplayGainError) as e: + raise ui.UserError( + u'replaygain initialization failed: {0}'.format(e)) + + self.r128_backend_instance.method = '--ebu' + def imported(self, session, task): """Add replay gain info to items or albums of ``task``. """ From 8168a88a7f6e401c7a982567f44d7e55e41f09d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Koutensk=C3=BD?= Date: Wed, 17 May 2017 21:58:41 +0200 Subject: [PATCH 46/71] update replaygain docs with info about r128 --- docs/plugins/replaygain.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/plugins/replaygain.rst b/docs/plugins/replaygain.rst index 6f119e2b06..838a2ea2b5 100644 --- a/docs/plugins/replaygain.rst +++ b/docs/plugins/replaygain.rst @@ -108,6 +108,10 @@ configuration file. The available options are: Default: ``no``. - **targetlevel**: A number of decibels for the target loudness level. Default: 89. +- **r128**: A space separated list of formats that will use ``R128_`` tags with + integer values instead of the common ``REPLAYGAIN_`` tags with floating point + values. Requires the "bs1770gain" backend. + Default: ``Opus``. These options only work with the "command" backend: From a93414dc9d3f801126e9d4bb47a35429aad9f1e0 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Thu, 25 May 2017 22:38:57 +1000 Subject: [PATCH 47/71] Document gapless mp3 encoding. --- docs/plugins/convert.rst | 49 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/docs/plugins/convert.rst b/docs/plugins/convert.rst index b8069506dd..99ce0de155 100644 --- a/docs/plugins/convert.rst +++ b/docs/plugins/convert.rst @@ -140,3 +140,52 @@ and the given command is used for all conversions. convert: command: ffmpeg -i $source -y -vn -aq 2 $dest extension: mp3 + + +Gapless MP3 encoding +```````````````````` + +Due to limitations in the ``ffmpeg`` encoder, by default the ``convert`` plugin +does not produce MP3s with accurate length tags (it does not write a LAME MP3 +info tag). This means that the MP3s are not "`gapless`_", and can result in +noticeable gaps between tracks during playback. + +.. _gapless: http://wiki.hydrogenaud.io/index.php?title=Gapless_playback + +To work around this problem, you must use the `LAME`_ MP3 encoder, which will +produce MP3s with accurate length tags. + +.. _LAME: http://lame.sourceforge.net/ + +To do this on Linux, you need to install ``lame``, create a script (e.g. +``/home/user/.config/beets/mp3.sh``) with the following contents, and make it +executable. + +:: + + #!/bin/sh + /usr/bin/ffmpeg -i "$1" -f wav - | /usr/bin/lame -V 2 --noreplaygain - "$2" + +Then configure the ``convert`` plugin to use the script. + +:: + + convert: + command: /home/user/.config/beets/mp3.sh $source $dest + extension: mp3 + +.. note:: + + In the script above, ``ffmpeg`` output format ``wav`` is used to produce an + accurate length header to pass to ``lame``. + + ``--noreplaygain`` configures ``lame`` not to do ReplayGain analysis and + tagging itself, because it can only do a track level analysis. Instead, you + can use the beets ``replaygain`` plugin to analyse and add both Album and + Track ReplayGain tags. + + See the ``lame`` `documentation`_ and the `HydrogenAudio wiki`_ for other + ``lame`` configuration options, and a thorough discussion of MP3 encoding. + +.. _documentation: http://lame.sourceforge.net/using.php +.. _HydrogenAudio wiki: http://wiki.hydrogenaud.io/index.php?title=LAME From 348e04464a3faf267b80c5678012c0d5388a9025 Mon Sep 17 00:00:00 2001 From: Scott Leggett Date: Fri, 26 May 2017 07:26:07 +1000 Subject: [PATCH 48/71] Remove absolute paths from sample script. --- docs/plugins/convert.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/convert.rst b/docs/plugins/convert.rst index 99ce0de155..89f114a11a 100644 --- a/docs/plugins/convert.rst +++ b/docs/plugins/convert.rst @@ -164,7 +164,7 @@ executable. :: #!/bin/sh - /usr/bin/ffmpeg -i "$1" -f wav - | /usr/bin/lame -V 2 --noreplaygain - "$2" + ffmpeg -i "$1" -f wav - | lame -V 2 --noreplaygain - "$2" Then configure the ``convert`` plugin to use the script. From e1101d4e95664ef593135b16c846886cb55532c8 Mon Sep 17 00:00:00 2001 From: discopatrick Date: Thu, 1 Jun 2017 12:33:23 +0100 Subject: [PATCH 49/71] Update assertion with correct error name --- test/test_datequery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_datequery.py b/test/test_datequery.py index 6cd97fd306..71daa42e44 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -195,7 +195,7 @@ def test_datetime_space_separator(self): self.assertEqual(date_query.interval.end, datetime(2000, 1, 1, 13)) def test_datetime_invalid_separator(self): - with self.assertRaises(InvalidQueryArgumentTypeError): + with self.assertRaises(InvalidQueryArgumentValueError): DateQuery('added', '2000-01-01x12') From 95eeec937c7466987cd53e39207d4fff41fc0332 Mon Sep 17 00:00:00 2001 From: discopatrick Date: Thu, 1 Jun 2017 13:11:40 +0100 Subject: [PATCH 50/71] Add docs for datetime queries --- docs/reference/query.rst | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/reference/query.rst b/docs/reference/query.rst index b4789aa106..70988f0267 100644 --- a/docs/reference/query.rst +++ b/docs/reference/query.rst @@ -188,6 +188,33 @@ Find all items with a file modification time between 2008-12-01 and $ beet ls 'mtime:2008-12-01..2008-12-02' +You can also add an optional time value to date queries, specifying hours, +minutes, and seconds. + +Times are separated from dates by a space, an uppercase 'T' or a lowercase +'t', for example: ``2008-12-01T23:59:59``. If you specify a time, then the +date must contain a year, month, and day. The minutes and seconds are +optional. + +Here is an example that finds all items added on 2008-12-01 at or after 22:00 +but before 23:00:: + + $ beet ls 'added:2008-12-01T22' + +Find all items added on or after 2008-12-01 22:45:: + + $ beet ls 'added:2008-12-01T22:45..' + +Find all items added on 2008-12-01, at or after 22:45:20 but before 22:45:41:: + + $ beet ls 'added:2008-12-01T22:45:20..2008-12-01T22:45:40' + +Examples of each time format:: + + $ beet ls 'added:2008-12-01T22:45:20' + $ beet ls 'added:2008-12-01t22:45:20' + $ beet ls 'added:2008-12-01 22:45:20' + .. _not_query: Query Term Negation From ee46a5150f4d97f03eaef509d098f739f3ae88ce Mon Sep 17 00:00:00 2001 From: Jakub Turski Date: Fri, 2 Jun 2017 15:48:05 +0100 Subject: [PATCH 51/71] Don't crash if non-canonical genre and prefer_specific: yes. Also, add a test for this. --- beetsplug/lastgenre/__init__.py | 1 + test/test_lastgenre.py | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/beetsplug/lastgenre/__init__.py b/beetsplug/lastgenre/__init__.py index 2756f452c0..4374310ba3 100644 --- a/beetsplug/lastgenre/__init__.py +++ b/beetsplug/lastgenre/__init__.py @@ -174,6 +174,7 @@ def _sort_by_depth(self, tags): genre tree. """ depth_tag_pairs = [(self._get_depth(t), t) for t in tags] + depth_tag_pairs = [e for e in depth_tag_pairs if e[0] is not None] depth_tag_pairs.sort(reverse=True) return [p[1] for p in depth_tag_pairs] diff --git a/test/test_lastgenre.py b/test/test_lastgenre.py index f57471f1cf..db2ea903ec 100644 --- a/test/test_lastgenre.py +++ b/test/test_lastgenre.py @@ -213,6 +213,17 @@ def mock_fetch_artist_genre(self, obj): self.assertEqual(res, (config['lastgenre']['fallback'].get(), u'fallback')) + def test_sort_by_depth(self): + self._setup_config(canonical=True,count=99) + # Normal case. + tags = ('electronic', 'ambient', 'post-rock', 'downtempo') + res = self.plugin._sort_by_depth(tags) + self.assertEqual(res, ['post-rock', 'downtempo', 'ambient', 'electronic']) + # Non-canonical tag ('chillout') present. + tags = ('electronic', 'ambient', 'chillout') + res = self.plugin._sort_by_depth(tags) + self.assertEqual(res, ['ambient', 'electronic']) + def suite(): return unittest.TestLoader().loadTestsFromName(__name__) From a43f5fdace8631b836998a040a25acd27731f565 Mon Sep 17 00:00:00 2001 From: Jakub Turski Date: Fri, 2 Jun 2017 15:50:58 +0100 Subject: [PATCH 52/71] Remove unnecessary test setup parameter. --- test/test_lastgenre.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_lastgenre.py b/test/test_lastgenre.py index db2ea903ec..d8ba60cf94 100644 --- a/test/test_lastgenre.py +++ b/test/test_lastgenre.py @@ -214,7 +214,7 @@ def mock_fetch_artist_genre(self, obj): u'fallback')) def test_sort_by_depth(self): - self._setup_config(canonical=True,count=99) + self._setup_config(canonical=True) # Normal case. tags = ('electronic', 'ambient', 'post-rock', 'downtempo') res = self.plugin._sort_by_depth(tags) From 0e7a0a62d43c23cb27b08c79246cffaa82a8f0a1 Mon Sep 17 00:00:00 2001 From: Jakub Turski Date: Fri, 2 Jun 2017 16:04:05 +0100 Subject: [PATCH 53/71] Fix excessive line length. --- test/test_lastgenre.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_lastgenre.py b/test/test_lastgenre.py index d8ba60cf94..18cb81a0a9 100644 --- a/test/test_lastgenre.py +++ b/test/test_lastgenre.py @@ -218,7 +218,8 @@ def test_sort_by_depth(self): # Normal case. tags = ('electronic', 'ambient', 'post-rock', 'downtempo') res = self.plugin._sort_by_depth(tags) - self.assertEqual(res, ['post-rock', 'downtempo', 'ambient', 'electronic']) + self.assertEqual( + res, ['post-rock', 'downtempo', 'ambient', 'electronic']) # Non-canonical tag ('chillout') present. tags = ('electronic', 'ambient', 'chillout') res = self.plugin._sort_by_depth(tags) From f6830b4bc3dafcef2f71a92904213e0905e02ae7 Mon Sep 17 00:00:00 2001 From: Jakub Turski Date: Fri, 2 Jun 2017 16:27:49 +0100 Subject: [PATCH 54/71] Here, flake8, be happy. --- test/test_lastgenre.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_lastgenre.py b/test/test_lastgenre.py index 18cb81a0a9..a70c65ca10 100644 --- a/test/test_lastgenre.py +++ b/test/test_lastgenre.py @@ -219,7 +219,7 @@ def test_sort_by_depth(self): tags = ('electronic', 'ambient', 'post-rock', 'downtempo') res = self.plugin._sort_by_depth(tags) self.assertEqual( - res, ['post-rock', 'downtempo', 'ambient', 'electronic']) + res, ['post-rock', 'downtempo', 'ambient', 'electronic']) # Non-canonical tag ('chillout') present. tags = ('electronic', 'ambient', 'chillout') res = self.plugin._sort_by_depth(tags) From 291b287f562b8d268a5495ffb9bc3dcae7c0429d Mon Sep 17 00:00:00 2001 From: discopatrick Date: Mon, 5 Jun 2017 16:31:37 +0100 Subject: [PATCH 55/71] Add a test for a non-range date query --- test/test_datequery.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/test_datequery.py b/test/test_datequery.py index 71daa42e44..ba88570840 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -75,6 +75,12 @@ def test_hour_precision_intervals(self): self.assertExcludes('2000-01-01T12..2000-01-01T13', '2000-01-01T14:30:00') + # test non-range query + self.assertContains('2008-12-01T22', + '2008-12-01T22:30:00') + self.assertExcludes('2008-12-01T22', + '2008-12-01T23:30:00') + def test_minute_precision_intervals(self): self.assertExcludes('2000-01-01T12:30..2000-01-01T12:31', '2000-01-01T12:29:59') From a559e6e14e439f28f8c44815c0b99525f3cb92c8 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 10 Jun 2017 16:25:36 -0400 Subject: [PATCH 56/71] Edit the changelog for release --- docs/changelog.rst | 59 +++++++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index caea9c6a4a..25f25eb86a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,39 +4,50 @@ Changelog 1.4.4 (in development) ---------------------- -New features: +This release built up a longer-than-normal list of nifty new features. We now +support DSF audio files and the importer can hard-link your files, for +example. + +Here's a full list of new features: * Added support for DSF files, once a future version of Mutagen is released that supports them. Thanks to :user:`docbobo`. :bug:`459` :bug:`2379` +* A new :ref:`hardlink` config option instructs the importer to create hard + links on filesystems that support them. Thanks to :user:`jacobwgillespie`. + :bug:`2445` +* A new :doc:`/plugins/kodiupdate` lets you keep your Kodi library in sync + with beets. Thanks to :user:`Pauligrinder`. :bug:`2411` +* A new :ref:`bell` configuration option under the ``import`` section enables + a terminal bell when input is required. Thanks to :user:`SpirosChadoulos`. + :bug:`2366` :bug:`2495` +* A new field, ``composer_sort``, is now supported and fetched from + MusicBrainz. + Thanks to :user:`dosoe`. + :bug:`2519` :bug:`2529` * The MusicBrainz backend and :doc:`/plugins/discogs` now both provide a new attribute called ``track_alt`` that stores more nuanced, possibly non-numeric track index data. For example, some vinyl or tape media will report the side of the record using a letter instead of a number in that field. :bug:`1831` :bug:`2363` -* The :doc:`/plugins/web` has a new endpoint, ``/item/path/foo``, which will +* :doc:`/plugins/web`: Added a new endpoint, ``/item/path/foo``, which will return the item info for the file at the given path, or 404. -* The :doc:`/plugins/web` also has a new config option, ``include_paths``, +* :doc:`/plugins/web`: Added a new config option, ``include_paths``, which will cause paths to be included in item API responses if set to true. * The ``%aunique`` template function for :ref:`aunique` now takes a third - argument that specifies which brackets to use around the disambiguator + argument that specifies which brackets to use around the disambiguator value. The argument can be any two characters that represent the left and right brackets. It defaults to `[]` and can also be blank to turn off bracketing. :bug:`2397` :bug:`2399` * Added a ``--move`` or ``-m`` option to the importer so that the files can be - moved to the library instead of being copied or added "in place". + moved to the library instead of being copied or added "in place." :bug:`2252` :bug:`2429` * :doc:`/plugins/badfiles`: Added a ``--verbose`` or ``-v`` option. Results are now displayed only for corrupted files by default and for all the files when the verbose option is set. :bug:`1654` :bug:`2434` -* A new :ref:`hardlink` config option instructs the importer to create hard - links on filesystems that support them. Thanks to :user:`jacobwgillespie`. - :bug:`2445` * :doc:`/plugins/embedart`: The explicit ``embedart`` command now asks for confirmation before embedding art into music files. Thanks to :user:`Stunner`. :bug:`1999` * You can now run beets by typing `python -m beets`. :bug:`2453` -* A new :doc:`/plugins/kodiupdate` lets you keep your Kodi library in sync - with beets. Thanks to :user:`Pauligrinder`. :bug:`2411` * :doc:`/plugins/smartplaylist`: Different playlist specifications that generate identically-named playlist files no longer conflict; instead, the resulting lists of tracks are concatenated. :bug:`2468` @@ -44,9 +55,6 @@ New features: you have in your library. Thanks to :user:`qlyoung`. :bug:`2481` * :doc:`/plugins/web` : Add new `reverse_proxy` config option to allow serving the web plugins under a reverse proxy. -* A new :ref:`bell` configuration option under the ``import`` section enables - a terminal bell when input is required. Thanks to :user:`SpirosChadoulos`. - :bug:`2366` :bug:`2495` * Importing a release with multiple release events now selects the event based on your :ref:`preferred` countries. :bug:`2501` * :doc:`/plugins/play`: A new ``-y`` or ``--yes`` parameter lets you skip @@ -57,28 +65,25 @@ New features: Thanks to :user:`jansol`. :bug:`2488` :bug:`2524` -* A new field, ``composer_sort``, is now supported and fetched from - MusicBrainz. - Thanks to :user:`dosoe`. - :bug:`2519` :bug:`2529` -Fixes: +There are also quite a few fixes: * In the :ref:`replace` configuration option, we now replace a leading hyphen (-) with an underscore. :bug:`549` :bug:`2509` -* :doc:`/plugins/absubmit`: Do not filter for supported formats. :bug:`2471` +* :doc:`/plugins/absubmit`: We no longer filter audio files for specific + formats---we will attempt the submission process for all formats. :bug:`2471` * :doc:`/plugins/mpdupdate`: Fix Python 3 compatibility. :bug:`2381` * :doc:`/plugins/replaygain`: Fix Python 3 compatibility in the ``bs1770gain`` backend. :bug:`2382` -* :doc:`/plugins/bpd`: Report playback times as integer. :bug:`2394` +* :doc:`/plugins/bpd`: Report playback times as integers. :bug:`2394` * :doc:`/plugins/mpdstats`: Fix Python 3 compatibility. The plugin also now requires version 0.4.2 or later of the ``python-mpd2`` library. :bug:`2405` -* :doc:`/plugins/mpdstats`: Improve handling of mpd status queries. +* :doc:`/plugins/mpdstats`: Improve handling of MPD status queries. * :doc:`/plugins/badfiles`: Fix Python 3 compatibility. * Fix some cases where album-level ReplayGain/SoundCheck metadata would be written to files incorrectly. :bug:`2426` -* :doc:`/plugins/badfiles`: The command no longer bails out if validator - command is not found or exists with an error. :bug:`2430` :bug:`2433` +* :doc:`/plugins/badfiles`: The command no longer bails out if the validator + command is not found or exits with an error. :bug:`2430` :bug:`2433` * :doc:`/plugins/lyrics`: The Google search backend no longer crashes when the server responds with an error. :bug:`2437` * :doc:`/plugins/discogs`: You can now authenticate with Discogs using a @@ -87,7 +92,7 @@ Fixes: Thanks to :user:`Lompik`. :bug:`2443` :bug:`2448` * :doc:`/plugins/duplicates`: Fix Python 3 compatibility when using the ``copy`` and ``move`` options. :bug:`2444` -* :doc:`/plugins/mbsubmit`: The tracks are now sorted. Thanks to +* :doc:`/plugins/mbsubmit`: The tracks are now sorted properly. Thanks to :user:`awesomer`. :bug:`2457` * :doc:`/plugins/thumbnails`: Fix a string-related crash on Python 3. :bug:`2466` @@ -98,8 +103,8 @@ Fixes: limited encoding. * :doc:`/plugins/convert`: The default configuration uses FFmpeg's built-in AAC codec instead of faac. Thanks to :user:`jansol`. :bug:`2484` -* Fix import of multidisc releases with subdirectories, which previously - made each disc be imported separately in different releases. :bug:`2493` +* Fix the importer's detection of multi-disc albums when other subdirectories + are present. :bug:`2493` * Invalid date queries now print an error message instead of being silently ignored. Thanks to :user:`discopatrick`. :bug:`2513` :bug:`2517` * When the SQLite database stops being accessible, we now print a friendly @@ -110,7 +115,7 @@ Fixes: * Fix a crash when reading non-ASCII characters in configuration files on Windows under Python 3. :bug:`2456` :bug:`2565` :bug:`2566` -Two plugins had backends removed due to bitrot: +We removed backends from two metadata plugins because of bitrot: * :doc:`/plugins/lyrics`: The Lyrics.com backend has been removed. (It stopped working because of changes to the site's URL structure.) From 9727ca72c946c44fbb7e8c8798b832eccb9b11d7 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 10 Jun 2017 16:28:17 -0400 Subject: [PATCH 57/71] Add date to the changelog --- docs/changelog.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 25f25eb86a..7016fe8547 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,8 +1,8 @@ Changelog ========= -1.4.4 (in development) ----------------------- +1.4.4 (June 10, 2017) +--------------------- This release built up a longer-than-normal list of nifty new features. We now support DSF audio files and the importer can hard-link your files, for From 8842bd3ecc9855faed1f2ba86239a27835a9e402 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 10 Jun 2017 16:28:50 -0400 Subject: [PATCH 58/71] Version bump: 1.4.5 --- beets/__init__.py | 2 +- docs/changelog.rst | 6 ++++++ docs/conf.py | 2 +- setup.py | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/beets/__init__.py b/beets/__init__.py index 964d2592c8..cab2560321 100644 --- a/beets/__init__.py +++ b/beets/__init__.py @@ -19,7 +19,7 @@ from beets.util import confit -__version__ = u'1.4.4' +__version__ = u'1.4.5' __author__ = u'Adrian Sampson ' diff --git a/docs/changelog.rst b/docs/changelog.rst index 7016fe8547..d253f6d063 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,12 @@ Changelog ========= +1.4.5 (in development) +---------------------- + +Changelog goes here! + + 1.4.4 (June 10, 2017) --------------------- diff --git a/docs/conf.py b/docs/conf.py index 9573b2fba1..cd55e57a5f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,7 +16,7 @@ copyright = u'2016, Adrian Sampson' version = '1.4' -release = '1.4.4' +release = '1.4.5' pygments_style = 'sphinx' diff --git a/setup.py b/setup.py index 1486438375..9fdbf63caa 100755 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ def build_manpages(): setup( name='beets', - version='1.4.4', + version='1.4.5', description='music tagger and library organizer', author='Adrian Sampson', author_email='adrian@radbox.org', From 29d6c27d02e000888a747e8bdcab03a7418f5137 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 10 Jun 2017 20:03:01 -0400 Subject: [PATCH 59/71] Fix some spurious whitespace changes --- test/test_ui.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_ui.py b/test/test_ui.py index f34cf72cb7..c67c9cb55d 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -156,6 +156,7 @@ def test_remove_items_with_force_delete(self): class ModifyTest(unittest.TestCase, TestHelper): + def setUp(self): self.setup_beets() self.album = self.add_album_fixture() @@ -346,6 +347,7 @@ def test_arg_parsing_equals_in_value(self): class WriteTest(unittest.TestCase, TestHelper): + def setUp(self): self.setup_beets() @@ -1248,7 +1250,6 @@ class CommonOptionsParserCliTest(unittest.TestCase, TestHelper): """Test CommonOptionsParser and formatting LibModel formatting on 'list' command. """ - def setUp(self): self.setup_beets() self.lib = library.Library(':memory:') @@ -1445,6 +1446,5 @@ def in_encoding_default_utf8(self): def suite(): return unittest.TestLoader().loadTestsFromName(__name__) - if __name__ == '__main__': unittest.main(defaultTest='suite') From b25eb87f6091d5c0fad0babf4efb6a15087958a5 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 10 Jun 2017 20:05:17 -0400 Subject: [PATCH 60/71] Remove unnecessary output capture --- test/test_ui.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/test_ui.py b/test/test_ui.py index c67c9cb55d..0fbec0bdef 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -481,8 +481,6 @@ class ExportTest(_common.TestCase): def setUp(self): super(ExportTest, self).setUp() - self.io.install() - self.libdir = os.path.join(self.temp_dir, b'testlibdir') os.mkdir(self.libdir) From 231528784d32927a72afce2116b6f07baba07ee6 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 10 Jun 2017 20:15:02 -0400 Subject: [PATCH 61/71] Simplify implementation of export behavior --- beets/ui/commands.py | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 7ffa90b11e..df23f58875 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -1464,6 +1464,7 @@ def move_items(lib, dest, query, copy, album, pretend, export=False, isalbummoved = lambda album: any(isitemmoved(i) for i in album.items()) objs = [o for o in objs if (isalbummoved if album else isitemmoved)(o)] + copy = copy or export # Exporting always copies. action = u'Copying' if copy else u'Moving' act = u'copy' if copy else u'move' entity = u'album' if album else u'item' @@ -1480,22 +1481,6 @@ def move_items(lib, dest, query, copy, album, pretend, export=False, show_path_changes([(obj.path, obj.destination(basedir=dest)) for obj in objs]) else: - # Copying files without modifying the database. - if export: - # export(file) function copies a file without modifying - # the database. - - def export(file): - # Create necessary ancestry for the copy. - util.mkdirall(file.destination(basedir=dest)) - util.copy(file.path, file.destination(basedir=dest)) - if album: - for obj in objs: - for item in obj.items(): - export(item) - else: - for obj in objs: - export(obj) if confirm: objs = ui.input_select_objects( u'Really %s' % act, objs, @@ -1505,8 +1490,12 @@ def export(file): for obj in objs: log.debug(u'moving: {0}', util.displayable_path(obj.path)) - obj.move(copy, basedir=dest) - obj.store() + if export: + # Copy without affecting the database. + obj.move(True, basedir=dest, store=False) + else: + # Ordinary move/copy: store the new path. + obj.move(copy, basedir=dest) def move_func(lib, opts, args): From 714560aace0bb53af3b594fc1ee43c4f0bed2e11 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 10 Jun 2017 20:19:27 -0400 Subject: [PATCH 62/71] Fix parameter order and binding The calls didn't match up with the parameter order. --- beets/ui/commands.py | 4 ++-- test/test_ui.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index df23f58875..d95a126d11 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -1450,8 +1450,8 @@ def modify_func(lib, opts, args): # move: Move/copy files to the library or a new base directory. -def move_items(lib, dest, query, copy, album, pretend, export=False, - confirm=False): +def move_items(lib, dest, query, copy, album, pretend, confirm=False, + export=False): """Moves or copies items to a new base directory, given by dest. If dest is None, then the library's base directory is used, making the command "consolidate" files. diff --git a/test/test_ui.py b/test/test_ui.py index 0fbec0bdef..a24a635f30 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -499,7 +499,7 @@ def setUp(self): def _move(self, query=(), dest=None, copy=False, album=False, pretend=False, export=True): commands.move_items(self.lib, dest, query, copy, album, - pretend, export) + pretend, export=export) def test_export_item(self): self._move() From 730c84e5e575488afb6a0b042582737f3fc2b2ea Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 10 Jun 2017 20:24:45 -0400 Subject: [PATCH 63/71] Correct tests for export mode The old tests were wrong but the incorrectness was hidden by the incorrect parameter passing fixed in the previous commit. Now we actually test that the item's path did not change. --- test/test_ui.py | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/test/test_ui.py b/test/test_ui.py index a24a635f30..ed31570e4b 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -501,36 +501,23 @@ def _move(self, query=(), dest=None, copy=False, album=False, commands.move_items(self.lib, dest, query, copy, album, pretend, export=export) - def test_export_item(self): - self._move() - self.i.load() - self.assertTrue(b'testlibdir' in self.i.path) - self.assertExists(self.i.path) - self.assertNotExists(self.itempath) - def test_export_item_custom_dir(self): self._move(dest=self.otherdir) self.i.load() - self.assertTrue(b'testotherdir' in self.i.path) - self.assertExists(self.i.path) - self.assertNotExists(self.itempath) + self.assertEqual(self.i.path, self.itempath) + self.assertExists(self.otherdir) def test_export_album_custom_dir(self): self._move(dest=self.otherdir, album=True) self.i.load() - self.assertTrue(b'testotherdir' in self.i.path) - self.assertExists(self.i.path) - self.assertNotExists(self.itempath) + self.assertEqual(self.i.path, self.itempath) + self.assertExists(self.otherdir) def test_pretend_export_item(self): self._move(dest=self.otherdir, pretend=True) self.i.load() self.assertIn(b'srcfile', self.i.path) - - def test_pretend_export_album(self): - self._move(album=True, pretend=True) - self.i.load() - self.assertIn(b'srcfile', self.i.path) + self.assertNotExists(self.otherdir) class UpdateTest(_common.TestCase): From ca4f96e33c0f13e1a17edfc96e94293d78ed8144 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 10 Jun 2017 20:26:41 -0400 Subject: [PATCH 64/71] Consolidate export tests into MoveTest Just one new flag. --- test/test_ui.py | 36 ++++++------------------------------ 1 file changed, 6 insertions(+), 30 deletions(-) diff --git a/test/test_ui.py b/test/test_ui.py index ed31570e4b..04c033f51a 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -421,8 +421,9 @@ def setUp(self): self.otherdir = os.path.join(self.temp_dir, b'testotherdir') def _move(self, query=(), dest=None, copy=False, album=False, - pretend=False): - commands.move_items(self.lib, dest, query, copy, album, pretend) + pretend=False, export=False): + commands.move_items(self.lib, dest, query, copy, album, pretend, + export=export) def test_move_item(self): self._move() @@ -476,45 +477,20 @@ def test_pretend_move_album(self): self.i.load() self.assertIn(b'srcfile', self.i.path) - -class ExportTest(_common.TestCase): - def setUp(self): - super(ExportTest, self).setUp() - - self.libdir = os.path.join(self.temp_dir, b'testlibdir') - os.mkdir(self.libdir) - - self.itempath = os.path.join(self.libdir, b'srcfile') - shutil.copy(os.path.join(_common.RSRC, b'full.mp3'), self.itempath) - - # Add a file to the library but don't copy it in yet. - self.lib = library.Library(':memory:', self.libdir) - self.i = library.Item.from_path(self.itempath) - self.lib.add(self.i) - self.album = self.lib.add_album([self.i]) - - # Alternate destination directory. - self.otherdir = os.path.join(self.temp_dir, b'testotherdir') - - def _move(self, query=(), dest=None, copy=False, album=False, - pretend=False, export=True): - commands.move_items(self.lib, dest, query, copy, album, - pretend, export=export) - def test_export_item_custom_dir(self): - self._move(dest=self.otherdir) + self._move(dest=self.otherdir, export=True) self.i.load() self.assertEqual(self.i.path, self.itempath) self.assertExists(self.otherdir) def test_export_album_custom_dir(self): - self._move(dest=self.otherdir, album=True) + self._move(dest=self.otherdir, album=True, export=True) self.i.load() self.assertEqual(self.i.path, self.itempath) self.assertExists(self.otherdir) def test_pretend_export_item(self): - self._move(dest=self.otherdir, pretend=True) + self._move(dest=self.otherdir, pretend=True, export=True) self.i.load() self.assertIn(b'srcfile', self.i.path) self.assertNotExists(self.otherdir) From 7cacae5c982046d33703923e33d53d92dd36d0c4 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 10 Jun 2017 20:35:09 -0400 Subject: [PATCH 65/71] Changelog for #2560 (fixes #2557) --- docs/changelog.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index d253f6d063..e8424f60af 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,7 +4,12 @@ Changelog 1.4.5 (in development) ---------------------- -Changelog goes here! +Features: + +* :doc:`/plugins/replaygain`: We now keep R128 data in separate tags from + classic ReplayGain data for formats that need it (namely, Ogg Opus). A new + `r128` configuration option enables this behavior for specific formats. + Thanks to :user:`autrimpo`. :bug:`2557` :bug:`2560` 1.4.4 (June 10, 2017) From f6dc98121741a5bbc7acfcf56608c997e5c2eecc Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 10 Jun 2017 20:42:18 -0400 Subject: [PATCH 66/71] Edit docs from #2576 for brevity Fixes #1741. --- docs/plugins/convert.rst | 45 +++++++++++----------------------------- 1 file changed, 12 insertions(+), 33 deletions(-) diff --git a/docs/plugins/convert.rst b/docs/plugins/convert.rst index 89f114a11a..59036a38be 100644 --- a/docs/plugins/convert.rst +++ b/docs/plugins/convert.rst @@ -145,47 +145,26 @@ and the given command is used for all conversions. Gapless MP3 encoding ```````````````````` -Due to limitations in the ``ffmpeg`` encoder, by default the ``convert`` plugin -does not produce MP3s with accurate length tags (it does not write a LAME MP3 -info tag). This means that the MP3s are not "`gapless`_", and can result in -noticeable gaps between tracks during playback. - -.. _gapless: http://wiki.hydrogenaud.io/index.php?title=Gapless_playback - -To work around this problem, you must use the `LAME`_ MP3 encoder, which will -produce MP3s with accurate length tags. - -.. _LAME: http://lame.sourceforge.net/ - -To do this on Linux, you need to install ``lame``, create a script (e.g. -``/home/user/.config/beets/mp3.sh``) with the following contents, and make it -executable. - -:: +While FFmpeg cannot produce "`gapless`_" MP3s by itself, you can create them +by using `LAME`_ directly. Use a shell script like this to pipe the output of +FFmpeg into the LAME tool:: #!/bin/sh ffmpeg -i "$1" -f wav - | lame -V 2 --noreplaygain - "$2" -Then configure the ``convert`` plugin to use the script. - -:: +Then configure the ``convert`` plugin to use the script:: convert: - command: /home/user/.config/beets/mp3.sh $source $dest + command: /path/to/script.sh $source $dest extension: mp3 -.. note:: - - In the script above, ``ffmpeg`` output format ``wav`` is used to produce an - accurate length header to pass to ``lame``. - - ``--noreplaygain`` configures ``lame`` not to do ReplayGain analysis and - tagging itself, because it can only do a track level analysis. Instead, you - can use the beets ``replaygain`` plugin to analyse and add both Album and - Track ReplayGain tags. - - See the ``lame`` `documentation`_ and the `HydrogenAudio wiki`_ for other - ``lame`` configuration options, and a thorough discussion of MP3 encoding. +This strategy configures FFmpeg to produce a WAV file with an accurate length +header for LAME to use. Using ``--noreplaygain`` disables gain analysis; you +can use the :doc:`/plugins/replaygain` to do this analysis. See the LAME +`documentation`_ and the `HydrogenAudio wiki`_ for other LAME configuration +options and a thorough discussion of MP3 encoding. .. _documentation: http://lame.sourceforge.net/using.php .. _HydrogenAudio wiki: http://wiki.hydrogenaud.io/index.php?title=LAME +.. _gapless: http://wiki.hydrogenaud.io/index.php?title=Gapless_playback +.. _LAME: http://lame.sourceforge.net/ From 1af3729b4e4263c9a1e0d045ef635f935e9d93d1 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 10 Jun 2017 20:48:07 -0400 Subject: [PATCH 67/71] Changelog for #2583 --- docs/changelog.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index e8424f60af..0aaf5f409f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,6 +11,12 @@ Features: `r128` configuration option enables this behavior for specific formats. Thanks to :user:`autrimpo`. :bug:`2557` :bug:`2560` +Fixes: + +* :doc:`/plugins/lastgenre`: Fix a crash when using the `prefer_specific` and + `canonical` options together. Thanks to :user:`yacoob`. :bug:`2459` + :bug:`2583` + 1.4.4 (June 10, 2017) --------------------- From f4d33686aea17fbc4dc08459bf1bc18eeb2b874b Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 10 Jun 2017 20:52:52 -0400 Subject: [PATCH 68/71] Some more clarity in comments for #2294 --- beets/autotag/mb.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index 8676126e9d..5c484d2b38 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -333,20 +333,23 @@ def album_info(release): disambig.append(release.get('disambiguation')) info.albumdisambig = u', '.join(disambig) - # Retrieves the Release type. - # Considers all other release types(including primary and secondary) (new) - # and logs them. + # Get the "classic" Release type. This data comes from a legacy API + # feature before MusicBrainz supported multiple release types. if 'type' in release['release-group']: reltype = release['release-group']['type'] if reltype: info.albumtype = reltype.lower() + + # Log the new-style "primary" and "secondary" release types. + # Eventually, we'd like to actually store this data, but we just log + # it for now to help understand the differences. if 'primary-type' in release['release-group']: rel_primarytype = release['release-group']['primary-type'] if rel_primarytype: - log.debug('Primary Type (new data): ' + rel_primarytype.lower()) + log.debug('primary MB release type: ' + rel_primarytype.lower()) if 'secondary-type-list' in release['release-group']: if release['release-group']['secondary-type-list']: - log.debug('Secondary Type(s) (new data): ' + ', '.join( + log.debug('secondary MB release type(s): ' + ', '.join( [secondarytype.lower() for secondarytype in release['release-group']['secondary-type-list']])) From d645c03ded42ce4b69057d717a9da7fd3871b75d Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 10 Jun 2017 20:59:02 -0400 Subject: [PATCH 69/71] Changelog for #2528 (fixes #2506) --- docs/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0aaf5f409f..9089058d20 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,9 @@ Changelog Features: +* :ref:`Date queries ` can now include times, so you can filter + your music down to the second. Thanks to :user:`discopatrick`. :bug:`2506` + :bug:`2528` * :doc:`/plugins/replaygain`: We now keep R128 data in separate tags from classic ReplayGain data for formats that need it (namely, Ogg Opus). A new `r128` configuration option enables this behavior for specific formats. From 0a731484e2a2a4401d59d9d25308f2b67cfcc351 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 10 Jun 2017 21:00:49 -0400 Subject: [PATCH 70/71] Slight refinements to time query docs --- docs/reference/query.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/reference/query.rst b/docs/reference/query.rst index 70988f0267..112c1966de 100644 --- a/docs/reference/query.rst +++ b/docs/reference/query.rst @@ -201,15 +201,17 @@ but before 23:00:: $ beet ls 'added:2008-12-01T22' -Find all items added on or after 2008-12-01 22:45:: +To find all items added on or after 2008-12-01 at 22:45:: $ beet ls 'added:2008-12-01T22:45..' -Find all items added on 2008-12-01, at or after 22:45:20 but before 22:45:41:: +To find all items added on 2008-12-01, at or after 22:45:20 but before +22:45:41:: $ beet ls 'added:2008-12-01T22:45:20..2008-12-01T22:45:40' -Examples of each time format:: +Here are example of the three ways to separate dates from times. All of these +queries do the same thing:: $ beet ls 'added:2008-12-01T22:45:20' $ beet ls 'added:2008-12-01t22:45:20' From f65653a843f45ece2f64c51cf5c347bf1a7c3604 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 11 Jun 2017 12:22:49 -0400 Subject: [PATCH 71/71] Changelog for #2510 (fixes #435) --- docs/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9089058d20..d50c25800b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -13,6 +13,9 @@ Features: classic ReplayGain data for formats that need it (namely, Ogg Opus). A new `r128` configuration option enables this behavior for specific formats. Thanks to :user:`autrimpo`. :bug:`2557` :bug:`2560` +* The :ref:`move-cmd` command gained a new ``--export`` flag, which copies + files to an external location without changing their location in the library + database. Thanks to :user:`SpirosChadoulos`. :bug:`435` :bug:`2510` Fixes: