beets-1.4.6/ 0000755 0000765 0000024 00000000000 13216774613 013602 5 ustar asampson staff 0000000 0000000 beets-1.4.6/man/ 0000755 0000765 0000024 00000000000 13216774613 014355 5 ustar asampson staff 0000000 0000000 beets-1.4.6/man/beetsconfig.5 0000644 0000765 0000024 00000076234 13216774613 016747 0 ustar asampson staff 0000000 0000000 .\" Man page generated from reStructuredText.
.
.TH "BEETSCONFIG" "5" "Dec 21, 2017" "1.4" "beets"
.SH NAME
beetsconfig \- beets configuration file
.
.nr rst2man-indent-level 0
.
.de1 rstReportMargin
\\$1 \\n[an-margin]
level \\n[rst2man-indent-level]
level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
-
\\n[rst2man-indent0]
\\n[rst2man-indent1]
\\n[rst2man-indent2]
..
.de1 INDENT
.\" .rstReportMargin pre:
. RS \\$1
. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin]
. nr rst2man-indent-level +1
.\" .rstReportMargin post:
..
.de UNINDENT
. RE
.\" indent \\n[an-margin]
.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]]
.nr rst2man-indent-level -1
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.sp
Beets has an extensive configuration system that lets you customize nearly
every aspect of its operation. To configure beets, you create a file called
\fBconfig.yaml\fP\&. The location of the file depend on your platform (type \fBbeet
config \-p\fP to see the path on your system):
.INDENT 0.0
.IP \(bu 2
On Unix\-like OSes, write \fB~/.config/beets/config.yaml\fP\&.
.IP \(bu 2
On Windows, use \fB%APPDATA%\ebeets\econfig.yaml\fP\&. This is usually in a
directory like \fBC:\eUsers\eYou\eAppData\eRoaming\fP\&.
.IP \(bu 2
On OS X, you can use either the Unix location or \fB~/Library/Application
Support/beets/config.yaml\fP\&.
.UNINDENT
.sp
You can launch your text editor to create or update your configuration by
typing \fBbeet config \-e\fP\&. (See the config\-cmd command for details.) It
is also possible to customize the location of the configuration file and even
use multiple layers of configuration. See \fI\%Configuration Location\fP, below.
.sp
The config file uses \fI\%YAML\fP syntax. You can use the full power of YAML, but
most configuration options are simple key/value pairs. This means your config
file will look like this:
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
option: value
another_option: foo
bigger_option:
key: value
foo: bar
.ft P
.fi
.UNINDENT
.UNINDENT
.sp
In YAML, you will need to use spaces (not tabs!) to indent some lines. If you
have questions about more sophisticated syntax, take a look at the \fI\%YAML\fP
documentation.
.sp
The rest of this page enumerates the dizzying litany of configuration options
available in beets. You might also want to see an
\fI\%example\fP\&.
.INDENT 0.0
.IP \(bu 2
\fI\%Global Options\fP
.INDENT 2.0
.IP \(bu 2
\fI\%library\fP
.IP \(bu 2
\fI\%directory\fP
.IP \(bu 2
\fI\%plugins\fP
.IP \(bu 2
\fI\%include\fP
.IP \(bu 2
\fI\%pluginpath\fP
.IP \(bu 2
\fI\%ignore\fP
.IP \(bu 2
\fI\%ignore_hidden\fP
.IP \(bu 2
\fI\%replace\fP
.IP \(bu 2
\fI\%asciify_paths\fP
.IP \(bu 2
\fI\%art_filename\fP
.IP \(bu 2
\fI\%threaded\fP
.IP \(bu 2
\fI\%format_item\fP
.IP \(bu 2
\fI\%format_album\fP
.IP \(bu 2
\fI\%sort_item\fP
.IP \(bu 2
\fI\%sort_album\fP
.IP \(bu 2
\fI\%sort_case_insensitive\fP
.IP \(bu 2
\fI\%original_date\fP
.IP \(bu 2
\fI\%per_disc_numbering\fP
.IP \(bu 2
\fI\%terminal_encoding\fP
.IP \(bu 2
\fI\%clutter\fP
.IP \(bu 2
\fI\%max_filename_length\fP
.IP \(bu 2
\fI\%id3v23\fP
.IP \(bu 2
\fI\%va_name\fP
.UNINDENT
.IP \(bu 2
\fI\%UI Options\fP
.INDENT 2.0
.IP \(bu 2
\fI\%color\fP
.IP \(bu 2
\fI\%colors\fP
.UNINDENT
.IP \(bu 2
\fI\%Importer Options\fP
.INDENT 2.0
.IP \(bu 2
\fI\%write\fP
.IP \(bu 2
\fI\%copy\fP
.IP \(bu 2
\fI\%move\fP
.IP \(bu 2
\fI\%link\fP
.IP \(bu 2
\fI\%hardlink\fP
.IP \(bu 2
\fI\%resume\fP
.IP \(bu 2
\fI\%incremental\fP
.IP \(bu 2
\fI\%from_scratch\fP
.IP \(bu 2
\fI\%quiet_fallback\fP
.IP \(bu 2
\fI\%none_rec_action\fP
.IP \(bu 2
\fI\%timid\fP
.IP \(bu 2
\fI\%log\fP
.IP \(bu 2
\fI\%default_action\fP
.IP \(bu 2
\fI\%languages\fP
.IP \(bu 2
\fI\%detail\fP
.IP \(bu 2
\fI\%group_albums\fP
.IP \(bu 2
\fI\%autotag\fP
.IP \(bu 2
\fI\%duplicate_action\fP
.IP \(bu 2
\fI\%bell\fP
.IP \(bu 2
\fI\%set_fields\fP
.UNINDENT
.IP \(bu 2
\fI\%MusicBrainz Options\fP
.INDENT 2.0
.IP \(bu 2
\fI\%searchlimit\fP
.UNINDENT
.IP \(bu 2
\fI\%Autotagger Matching Options\fP
.INDENT 2.0
.IP \(bu 2
\fI\%max_rec\fP
.IP \(bu 2
\fI\%preferred\fP
.IP \(bu 2
\fI\%ignored\fP
.IP \(bu 2
\fI\%required\fP
.UNINDENT
.IP \(bu 2
\fI\%Path Format Configuration\fP
.IP \(bu 2
\fI\%Configuration Location\fP
.INDENT 2.0
.IP \(bu 2
\fI\%Environment Variable\fP
.IP \(bu 2
\fI\%Command\-Line Option\fP
.IP \(bu 2
\fI\%Default Location\fP
.UNINDENT
.IP \(bu 2
\fI\%Example\fP
.UNINDENT
.SH GLOBAL OPTIONS
.sp
These options control beets’ global operation.
.SS library
.sp
Path to the beets library file. By default, beets will use a file called
\fBlibrary.db\fP alongside your configuration file.
.SS directory
.sp
The directory to which files will be copied/moved when adding them to the
library. Defaults to a folder called \fBMusic\fP in your home directory.
.SS plugins
.sp
A space\-separated list of plugin module names to load. See
using\-plugins\&.
.SS include
.sp
A space\-separated list of extra configuration files to include.
Filenames are relative to the directory containing \fBconfig.yaml\fP\&.
.SS pluginpath
.sp
Directories to search for plugins. Each Python file or directory in a plugin
path represents a plugin and should define a subclass of \fBBeetsPlugin\fP\&.
A plugin can then be loaded by adding the filename to the \fIplugins\fP configuration.
The plugin path can either be a single string or a list of strings—so, if you
have multiple paths, format them as a YAML list like so:
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
pluginpath:
\- /path/one
\- /path/two
.ft P
.fi
.UNINDENT
.UNINDENT
.SS ignore
.sp
A list of glob patterns specifying file and directory names to be ignored when
importing. By default, this consists of \fB\&.*\fP, \fB*~\fP, \fBSystem Volume
Information\fP, \fBlost+found\fP (i.e., beets ignores Unix\-style hidden files,
backup files, and directories that appears at the root of some Linux and Windows
filesystems).
.SS ignore_hidden
.sp
Either \fByes\fP or \fBno\fP; whether to ignore hidden files when importing. On
Windows, the “Hidden” property of files is used to detect whether or not a file
is hidden. On OS X, the file’s “IsHidden” flag is used to detect whether or not
a file is hidden. On both OS X and other platforms (excluding Windows), files
(and directories) starting with a dot are detected as hidden files.
.SS replace
.sp
A set of regular expression/replacement pairs to be applied to all filenames
created by beets. Typically, these replacements are used to avoid confusing
problems or errors with the filesystem (for example, leading dots, which hide
files on Unix, and trailing whitespace, which is illegal on Windows). To
override these substitutions, specify a mapping from regular expression to
replacement strings. For example, \fB[xy]: z\fP will make beets replace all
instances of the characters \fBx\fP or \fBy\fP with the character \fBz\fP\&.
.sp
If you do change this value, be certain that you include at least enough
substitutions to avoid causing errors on your operating system. Here are
the default substitutions used by beets, which are sufficient to avoid
unexpected behavior on all popular platforms:
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
replace:
\(aq[\e\e/]\(aq: _
\(aq^\e.\(aq: _
\(aq[\ex00\-\ex1f]\(aq: _
\(aq[<>:"\e?\e*\e|]\(aq: _
\(aq\e.$\(aq: _
\(aq\es+$\(aq: \(aq\(aq
\(aq^\es+\(aq: \(aq\(aq
\(aq^\-\(aq: _
.ft P
.fi
.UNINDENT
.UNINDENT
.sp
These substitutions remove forward and back slashes, leading dots, and
control characters—all of which is a good idea on any OS. The fourth line
removes the Windows “reserved characters” (useful even on Unix for for
compatibility with Windows\-influenced network filesystems like Samba).
Trailing dots and trailing whitespace, which can cause problems on Windows
clients, are also removed.
.sp
When replacements other than the defaults are used, it is possible that they
will increase the length of the path. In the scenario where this leads to a
conflict with the maximum filename length, the default replacements will be
used to resolve the conflict and beets will display a warning.
.sp
Note that paths might contain special characters such as typographical
quotes (\fB“”\fP). With the configuration above, those will not be
replaced as they don’t match the typewriter quote (\fB"\fP). To also strip these
special characters, you can either add them to the replacement list or use the
\fI\%asciify_paths\fP configuration option below.
.SS asciify_paths
.sp
Convert all non\-ASCII characters in paths to ASCII equivalents.
.sp
For example, if your path template for
singletons is \fBsingletons/$title\fP and the title of a track is “Café”,
then the track will be saved as \fBsingletons/Cafe.mp3\fP\&. The changes
take place before applying the \fI\%replace\fP configuration and are roughly
equivalent to wrapping all your path templates in the \fB%asciify{}\fP
template function\&.
.sp
Default: \fBno\fP\&.
.SS art_filename
.sp
When importing album art, the name of the file (without extension) where the
cover art image should be placed. This is a template string, so you can use any
of the syntax available to /reference/pathformat\&. Defaults to \fBcover\fP
(i.e., images will be named \fBcover.jpg\fP or \fBcover.png\fP and placed in the
album’s directory).
.SS threaded
.sp
Either \fByes\fP or \fBno\fP, indicating whether the autotagger should use
multiple threads. This makes things substantially faster by overlapping work:
for example, it can copy files for one album in parallel with looking up data
in MusicBrainz for a different album. You may want to disable this when
debugging problems with the autotagger.
Defaults to \fByes\fP\&.
.SS format_item
.sp
Format to use when listing \fIindividual items\fP with the list\-cmd
command and other commands that need to print out items. Defaults to
\fB$artist \- $album \- $title\fP\&. The \fB\-f\fP command\-line option overrides
this setting.
.sp
It used to be named \fIlist_format_item\fP\&.
.SS format_album
.sp
Format to use when listing \fIalbums\fP with list\-cmd and other
commands. Defaults to \fB$albumartist \- $album\fP\&. The \fB\-f\fP command\-line
option overrides this setting.
.sp
It used to be named \fIlist_format_album\fP\&.
.SS sort_item
.sp
Default sort order to use when fetching items from the database. Defaults to
\fBartist+ album+ disc+ track+\fP\&. Explicit sort orders override this default.
.SS sort_album
.sp
Default sort order to use when fetching albums from the database. Defaults to
\fBalbumartist+ album+\fP\&. Explicit sort orders override this default.
.SS sort_case_insensitive
.sp
Either \fByes\fP or \fBno\fP, indicating whether the case should be ignored when
sorting lexicographic fields. When set to \fBno\fP, lower\-case values will be
placed after upper\-case values (e.g., \fIBar Qux foo\fP), while \fByes\fP would
result in the more expected \fIBar foo Qux\fP\&. Default: \fByes\fP\&.
.SS original_date
.sp
Either \fByes\fP or \fBno\fP, indicating whether matched albums should have their
\fByear\fP, \fBmonth\fP, and \fBday\fP fields set to the release date of the
\fIoriginal\fP version of an album rather than the selected version of the release.
That is, if this option is turned on, then \fByear\fP will always equal
\fBoriginal_year\fP and so on. Default: \fBno\fP\&.
.SS per_disc_numbering
.sp
A boolean controlling the track numbering style on multi\-disc releases. By
default (\fBper_disc_numbering: no\fP), tracks are numbered per\-release, so the
first track on the second disc has track number N+1 where N is the number of
tracks on the first disc. If this \fBper_disc_numbering\fP is enabled, then the
first (non\-pregap) track on each disc always has track number 1.
.sp
If you enable \fBper_disc_numbering\fP, you will likely want to change your
\fI\%Path Format Configuration\fP also to include \fB$disc\fP before \fB$track\fP to make
filenames sort correctly in album directories. For example, you might want to
use a path format like this:
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
paths:
default: $albumartist/$album%aunique{}/$disc\-$track $title
.ft P
.fi
.UNINDENT
.UNINDENT
.sp
When this option is off (the default), even “pregap” hidden tracks are
numbered from one, not zero, so other track numbers may appear to be bumped up
by one. When it is on, the pregap track for each disc can be numbered zero.
.SS terminal_encoding
.sp
The text encoding, as \fI\%known to Python\fP, to use for messages printed to the
standard output. It’s also used to read messages from the standard input.
By default, this is determined automatically from the locale
environment variables.
.SS clutter
.sp
When beets imports all the files in a directory, it tries to remove the
directory if it’s empty. A directory is considered empty if it only contains
files whose names match the glob patterns in \fIclutter\fP, which should be a list
of strings. The default list consists of “Thumbs.DB” and “.DS_Store”.
.sp
The importer only removes recursively searched subdirectories—the top\-level
directory you specify on the command line is never deleted.
.SS max_filename_length
.sp
Set the maximum number of characters in a filename, after which names will be
truncated. By default, beets tries to ask the filesystem for the correct
maximum.
.SS id3v23
.sp
By default, beets writes MP3 tags using the ID3v2.4 standard, the latest
version of ID3. Enable this option to instead use the older ID3v2.3 standard,
which is preferred by certain older software such as Windows Media Player.
.SS va_name
.sp
Sets the albumartist for various\-artist compilations. Defaults to \fB\(aqVarious
Artists\(aq\fP (the MusicBrainz standard). Affects other sources, such as
/plugins/discogs, too.
.SH UI OPTIONS
.sp
The options that allow for customization of the visual appearance
of the console interface.
.sp
These options are available in this section:
.SS color
.sp
Either \fByes\fP or \fBno\fP; whether to use color in console output (currently
only in the \fBimport\fP command). Turn this off if your terminal doesn’t
support ANSI colors.
.sp
\fBNOTE:\fP
.INDENT 0.0
.INDENT 3.5
The \fIcolor\fP option was previously a top\-level configuration. This is
still respected, but a deprecation message will be shown until your
top\-level \fIcolor\fP configuration has been nested under \fIui\fP\&.
.UNINDENT
.UNINDENT
.SS colors
.sp
The colors that are used throughout the user interface. These are only used if
the \fBcolor\fP option is set to \fByes\fP\&. For example, you might have a section
in your configuration file that looks like this:
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
ui:
color: yes
colors:
text_success: green
text_warning: yellow
text_error: red
text_highlight: red
text_highlight_minor: lightgray
action_default: turquoise
action: blue
.ft P
.fi
.UNINDENT
.UNINDENT
.sp
Available colors: black, darkred, darkgreen, brown (darkyellow), darkblue,
purple (darkmagenta), teal (darkcyan), lightgray, darkgray, red, green,
yellow, blue, fuchsia (magenta), turquoise (cyan), white
.SH IMPORTER OPTIONS
.sp
The options that control the import\-cmd command are indented under the
\fBimport:\fP key. For example, you might have a section in your configuration
file that looks like this:
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
import:
write: yes
copy: yes
resume: no
.ft P
.fi
.UNINDENT
.UNINDENT
.sp
These options are available in this section:
.SS write
.sp
Either \fByes\fP or \fBno\fP, controlling whether metadata (e.g., ID3) tags are
written to files when using \fBbeet import\fP\&. Defaults to \fByes\fP\&. The \fB\-w\fP
and \fB\-W\fP command\-line options override this setting.
.SS copy
.sp
Either \fByes\fP or \fBno\fP, indicating whether to \fBcopy\fP files into the
library directory when using \fBbeet import\fP\&. Defaults to \fByes\fP\&. Can be
overridden with the \fB\-c\fP and \fB\-C\fP command\-line options.
.sp
The option is ignored if \fBmove\fP is enabled (i.e., beets can move or
copy files but it doesn’t make sense to do both).
.SS move
.sp
Either \fByes\fP or \fBno\fP, indicating whether to \fBmove\fP files into the
library directory when using \fBbeet import\fP\&.
Defaults to \fBno\fP\&.
.sp
The effect is similar to the \fBcopy\fP option but you end up with only
one copy of the imported file. (“Moving” works even across filesystems; if
necessary, beets will copy and then delete when a simple rename is
impossible.) Moving files can be risky—it’s a good idea to keep a backup in
case beets doesn’t do what you expect with your files.
.sp
This option \fIoverrides\fP \fBcopy\fP, so enabling it will always move
(and not copy) files. The \fB\-c\fP switch to the \fBbeet import\fP command,
however, still takes precedence.
.SS link
.sp
Either \fByes\fP or \fBno\fP, indicating whether to use symbolic links instead of
moving or copying files. (It conflicts with the \fBmove\fP, \fBcopy\fP and
\fBhardlink\fP options.) Defaults to \fBno\fP\&.
.sp
This option only works on platforms that support symbolic links: i.e., Unixes.
It will fail on Windows.
.sp
It’s likely that you’ll also want to set \fBwrite\fP to \fBno\fP if you use this
option to preserve the metadata on the linked files.
.SS hardlink
.sp
Either \fByes\fP or \fBno\fP, indicating whether to use hard links instead of
moving or copying or symlinking files. (It conflicts with the \fBmove\fP,
\fBcopy\fP, and \fBlink\fP options.) Defaults to \fBno\fP\&.
.sp
As with symbolic links (see \fI\%link\fP, above), this will not work on Windows
and you will want to set \fBwrite\fP to \fBno\fP\&. Otherwise, metadata on the
original file will be modified.
.SS resume
.sp
Either \fByes\fP, \fBno\fP, or \fBask\fP\&. Controls whether interrupted imports
should be resumed. “Yes” means that imports are always resumed when
possible; “no” means resuming is disabled entirely; “ask” (the default)
means that the user should be prompted when resuming is possible. The \fB\-p\fP
and \fB\-P\fP flags correspond to the “yes” and “no” settings and override this
option.
.SS incremental
.sp
Either \fByes\fP or \fBno\fP, controlling whether imported directories are
recorded and whether these recorded directories are skipped. This
corresponds to the \fB\-i\fP flag to \fBbeet import\fP\&.
.SS from_scratch
.sp
Either \fByes\fP or \fBno\fP (default), controlling whether existing metadata is
discarded when a match is applied. This corresponds to the \fB\-\-from_scratch\fP
flag to \fBbeet import\fP\&.
.SS quiet_fallback
.sp
Either \fBskip\fP (default) or \fBasis\fP, specifying what should happen in
quiet mode (see the \fB\-q\fP flag to \fBimport\fP, above) when there is no
strong recommendation.
.SS none_rec_action
.sp
Either \fBask\fP (default), \fBasis\fP or \fBskip\fP\&. Specifies what should happen
during an interactive import session when there is no recommendation. Useful
when you are only interested in processing medium and strong recommendations
interactively.
.SS timid
.sp
Either \fByes\fP or \fBno\fP, controlling whether the importer runs in \fItimid\fP
mode, in which it asks for confirmation on every autotagging match, even the
ones that seem very close. Defaults to \fBno\fP\&. The \fB\-t\fP command\-line flag
controls the same setting.
.SS log
.sp
Specifies a filename where the importer’s log should be kept. By default,
no log is written. This can be overridden with the \fB\-l\fP flag to
\fBimport\fP\&.
.SS default_action
.sp
One of \fBapply\fP, \fBskip\fP, \fBasis\fP, or \fBnone\fP, indicating which option
should be the \fIdefault\fP when selecting an action for a given match. This is the
action that will be taken when you type return without an option letter. The
default is \fBapply\fP\&.
.SS languages
.sp
A list of locale names to search for preferred aliases. For example, setting
this to “en” uses the transliterated artist name “Pyotr Ilyich Tchaikovsky”
instead of the Cyrillic script for the composer’s name when tagging from
MusicBrainz. Defaults to an empty list, meaning that no language is preferred.
.SS detail
.sp
Whether the importer UI should show detailed information about each match it
finds. When enabled, this mode prints out the title of every track, regardless
of whether it matches the original metadata. (The default behavior only shows
changes.) Default: \fBno\fP\&.
.SS group_albums
.sp
By default, the beets importer groups tracks into albums based on the
directories they reside in. This option instead uses files’ metadata to
partition albums. Enable this option if you have directories that contain
tracks from many albums mixed together.
.sp
The \fB\-\-group\-albums\fP or \fB\-g\fP option to the import\-cmd command is
equivalent, and the \fIG\fP interactive option invokes the same workflow.
.sp
Default: \fBno\fP\&.
.SS autotag
.sp
By default, the beets importer always attempts to autotag new music. If
most of your collection consists of obscure music, you may be interested in
disabling autotagging by setting this option to \fBno\fP\&. (You can re\-enable it
with the \fB\-a\fP flag to the import\-cmd command.)
.sp
Default: \fByes\fP\&.
.SS duplicate_action
.sp
Either \fBskip\fP, \fBkeep\fP, \fBremove\fP, \fBmerge\fP or \fBask\fP\&.
Controls how duplicates are treated in import task.
“skip” means that new item(album or track) will be skipped;
“keep” means keep both old and new items; “remove” means remove old
item; “merge” means merge into one album; “ask” means the user
should be prompted for the action each time. The default is \fBask\fP\&.
.SS bell
.sp
Ring the terminal bell to get your attention when the importer needs your input.
.sp
Default: \fBno\fP\&.
.SS set_fields
.sp
A dictionary indicating fields to set to values for newly imported music.
Here’s an example:
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
set_fields:
genre: \(aqTo Listen\(aq
collection: \(aqUnordered\(aq
.ft P
.fi
.UNINDENT
.UNINDENT
.sp
Other field/value pairs supplied via the \fB\-\-set\fP option on the command\-line
override any settings here for fields with the same name.
.sp
Default: \fB{}\fP (empty).
.SH MUSICBRAINZ OPTIONS
.sp
If you run your own \fI\%MusicBrainz\fP server, you can instruct beets to use it
instead of the main server. Use the \fBhost\fP and \fBratelimit\fP options under a
\fBmusicbrainz:\fP header, like so:
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
musicbrainz:
host: localhost:5000
ratelimit: 100
.ft P
.fi
.UNINDENT
.UNINDENT
.sp
The \fBhost\fP key, of course, controls the Web server hostname (and port,
optionally) that will be contacted by beets (default: musicbrainz.org). The
\fBratelimit\fP option, an integer, controls the number of Web service requests
per second (default: 1). \fBDo not change the rate limit setting\fP if you’re
using the main MusicBrainz server—on this public server, you’re \fI\%limited\fP
to one request per second.
.SS searchlimit
.sp
The number of matches returned when sending search queries to the
MusicBrainz server.
.sp
Default: \fB5\fP\&.
.SH AUTOTAGGER MATCHING OPTIONS
.sp
You can configure some aspects of the logic beets uses when automatically
matching MusicBrainz results under the \fBmatch:\fP section. To control how
\fItolerant\fP the autotagger is of differences, use the \fBstrong_rec_thresh\fP
option, which reflects the distance threshold below which beets will make a
“strong recommendation” that the metadata be used. Strong recommendations
are accepted automatically (except in “timid” mode), so you can use this to
make beets ask your opinion more or less often.
.sp
The threshold is a \fIdistance\fP value between 0.0 and 1.0, so you can think of it
as the opposite of a \fIsimilarity\fP value. For example, if you want to
automatically accept any matches above 90% similarity, use:
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
match:
strong_rec_thresh: 0.10
.ft P
.fi
.UNINDENT
.UNINDENT
.sp
The default strong recommendation threshold is 0.04.
.sp
The \fBmedium_rec_thresh\fP and \fBrec_gap_thresh\fP options work similarly. When a
match is below the \fImedium\fP recommendation threshold or the distance between it
and the next\-best match is above the \fIgap\fP threshold, the importer will suggest
that match but not automatically confirm it. Otherwise, you’ll see a list of
options to choose from.
.SS max_rec
.sp
As mentioned above, autotagger matches have \fIrecommendations\fP that control how
the UI behaves for a certain quality of match. The recommendation for a certain
match is based on the overall distance calculation. But you can also control
the recommendation when a specific distance penalty is applied by defining
\fImaximum\fP recommendations for each field:
.sp
To define maxima, use keys under \fBmax_rec:\fP in the \fBmatch\fP section. The
defaults are “medium” for missing and unmatched tracks and “strong” (i.e., no
maximum) for everything else:
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
match:
max_rec:
missing_tracks: medium
unmatched_tracks: medium
.ft P
.fi
.UNINDENT
.UNINDENT
.sp
If a recommendation is higher than the configured maximum and the indicated
penalty is applied, the recommendation is downgraded. The setting for
each field can be one of \fBnone\fP, \fBlow\fP, \fBmedium\fP or \fBstrong\fP\&. When the
maximum recommendation is \fBstrong\fP, no “downgrading” occurs. The available
penalty names here are:
.INDENT 0.0
.IP \(bu 2
source
.IP \(bu 2
artist
.IP \(bu 2
album
.IP \(bu 2
media
.IP \(bu 2
mediums
.IP \(bu 2
year
.IP \(bu 2
country
.IP \(bu 2
label
.IP \(bu 2
catalognum
.IP \(bu 2
albumdisambig
.IP \(bu 2
album_id
.IP \(bu 2
tracks
.IP \(bu 2
missing_tracks
.IP \(bu 2
unmatched_tracks
.IP \(bu 2
track_title
.IP \(bu 2
track_artist
.IP \(bu 2
track_index
.IP \(bu 2
track_length
.IP \(bu 2
track_id
.UNINDENT
.SS preferred
.sp
In addition to comparing the tagged metadata with the match metadata for
similarity, you can also specify an ordered list of preferred countries and
media types.
.sp
A distance penalty will be applied if the country or media type from the match
metadata doesn’t match. The specified values are preferred in descending order
(i.e., the first item will be most preferred). Each item may be a regular
expression, and will be matched case insensitively. The number of media will
be stripped when matching preferred media (e.g. “2x” in “2xCD”).
.sp
You can also tell the autotagger to prefer matches that have a release year
closest to the original year for an album.
.sp
Here’s an example:
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
match:
preferred:
countries: [\(aqUS\(aq, \(aqGB|UK\(aq]
media: [\(aqCD\(aq, \(aqDigital Media|File\(aq]
original_year: yes
.ft P
.fi
.UNINDENT
.UNINDENT
.sp
By default, none of these options are enabled.
.SS ignored
.sp
You can completely avoid matches that have certain penalties applied by adding
the penalty name to the \fBignored\fP setting:
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
match:
ignored: missing_tracks unmatched_tracks
.ft P
.fi
.UNINDENT
.UNINDENT
.sp
The available penalties are the same as those for the \fI\%max_rec\fP setting.
.SS required
.sp
You can avoid matches that lack certain required information. Add the tags you
want to enforce to the \fBrequired\fP setting:
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
match:
required: year label catalognum country
.ft P
.fi
.UNINDENT
.UNINDENT
.sp
No tags are required by default.
.SH PATH FORMAT CONFIGURATION
.sp
You can also configure the directory hierarchy beets uses to store music.
These settings appear under the \fBpaths:\fP key. Each string is a template
string that can refer to metadata fields like \fB$artist\fP or \fB$title\fP\&. The
filename extension is added automatically. At the moment, you can specify three
special paths: \fBdefault\fP for most releases, \fBcomp\fP for “various artist”
releases with no dominant artist, and \fBsingleton\fP for non\-album tracks. The
defaults look like this:
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
paths:
default: $albumartist/$album%aunique{}/$track $title
singleton: Non\-Album/$artist/$title
comp: Compilations/$album%aunique{}/$track $title
.ft P
.fi
.UNINDENT
.UNINDENT
.sp
Note the use of \fB$albumartist\fP instead of \fB$artist\fP; this ensures that albums
will be well\-organized. For more about these format strings, see
pathformat\&. The \fBaunique{}\fP function ensures that identically\-named
albums are placed in different directories; see aunique for details.
.sp
In addition to \fBdefault\fP, \fBcomp\fP, and \fBsingleton\fP, you can condition path
queries based on beets queries (see /reference/query). This means that a
config file like this:
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
paths:
albumtype:soundtrack: Soundtracks/$album/$track $title
.ft P
.fi
.UNINDENT
.UNINDENT
.sp
will place soundtrack albums in a separate directory. The queries are tested in
the order they appear in the configuration file, meaning that if an item matches
multiple queries, beets will use the path format for the \fIfirst\fP matching query.
.sp
Note that the special \fBsingleton\fP and \fBcomp\fP path format conditions are, in
fact, just shorthand for the explicit queries \fBsingleton:true\fP and
\fBcomp:true\fP\&. In contrast, \fBdefault\fP is special and has no query equivalent:
the \fBdefault\fP format is only used if no queries match.
.SH CONFIGURATION LOCATION
.sp
The beets configuration file is usually located in a standard location that
depends on your OS, but there are a couple of ways you can tell beets where to
look.
.SS Environment Variable
.sp
First, you can set the \fBBEETSDIR\fP environment variable to a directory
containing a \fBconfig.yaml\fP file. This replaces your configuration in the
default location. This also affects where auxiliary files, like the library
database, are stored by default (that’s where relative paths are resolved to).
This environment variable is useful if you need to manage multiple beets
libraries with separate configurations.
.SS Command\-Line Option
.sp
Alternatively, you can use the \fB\-\-config\fP command\-line option to indicate a
YAML file containing options that will then be merged with your existing
options (from \fBBEETSDIR\fP or the default locations). This is useful if you
want to keep your configuration mostly the same but modify a few options as a
batch. For example, you might have different strategies for importing files,
each with a different set of importer options.
.SS Default Location
.sp
In the absence of a \fBBEETSDIR\fP variable, beets searches a few places for
your configuration, depending on the platform:
.INDENT 0.0
.IP \(bu 2
On Unix platforms, including OS X:\fB~/.config/beets\fP and then
\fB$XDG_CONFIG_DIR/beets\fP, if the environment variable is set.
.IP \(bu 2
On OS X, we also search \fB~/Library/Application Support/beets\fP before the
Unixy locations.
.IP \(bu 2
On Windows: \fB~\eAppData\eRoaming\ebeets\fP, and then \fB%APPDATA%\ebeets\fP, if
the environment variable is set.
.UNINDENT
.sp
Beets uses the first directory in your platform’s list that contains
\fBconfig.yaml\fP\&. If no config file exists, the last path in the list is used.
.SH EXAMPLE
.sp
Here’s an example file:
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
directory: /var/mp3
import:
copy: yes
write: yes
log: beetslog.txt
art_filename: albumart
plugins: bpd
pluginpath: ~/beets/myplugins
ui:
color: yes
paths:
default: $genre/$albumartist/$album/$track $title
singleton: Singletons/$artist \- $title
comp: $genre/$album/$track $title
albumtype:soundtrack: Soundtracks/$album/$track $title
.ft P
.fi
.UNINDENT
.UNINDENT
.SH SEE ALSO
.sp
\fBhttp://beets.readthedocs.org/\fP
.sp
\fBbeet(1)\fP
.SH AUTHOR
Adrian Sampson
.SH COPYRIGHT
2016, Adrian Sampson
.\" Generated by docutils manpage writer.
.
beets-1.4.6/man/beet.1 0000644 0000765 0000024 00000045072 13216774613 015366 0 ustar asampson staff 0000000 0000000 .\" Man page generated from reStructuredText.
.
.TH "BEET" "1" "Dec 21, 2017" "1.4" "beets"
.SH NAME
beet \- music tagger and library organizer
.
.nr rst2man-indent-level 0
.
.de1 rstReportMargin
\\$1 \\n[an-margin]
level \\n[rst2man-indent-level]
level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
-
\\n[rst2man-indent0]
\\n[rst2man-indent1]
\\n[rst2man-indent2]
..
.de1 INDENT
.\" .rstReportMargin pre:
. RS \\$1
. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin]
. nr rst2man-indent-level +1
.\" .rstReportMargin post:
..
.de UNINDENT
. RE
.\" indent \\n[an-margin]
.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]]
.nr rst2man-indent-level -1
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.SH SYNOPSIS
.nf
\fBbeet\fP [\fIargs\fP…] \fIcommand\fP [\fIargs\fP…]
\fBbeet help\fP \fIcommand\fP
.fi
.sp
.SH COMMANDS
.SS import
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
beet import [\-CWAPRqst] [\-l LOGPATH] PATH...
beet import [options] \-L QUERY
.ft P
.fi
.UNINDENT
.UNINDENT
.sp
Add music to your library, attempting to get correct tags for it from
MusicBrainz.
.sp
Point the command at some music: directories, single files, or
compressed archives. The music will be copied to a configurable
directory structure and added to a library database. The command is
interactive and will try to get you to verify MusicBrainz tags that it
thinks are suspect. See the autotagging guide
for detail on how to use the interactive tag\-correction flow.
.sp
Directories passed to the import command can contain either a single
album or many, in which case the leaf directories will be considered
albums (the latter case is true of typical Artist/Album organizations
and many people’s “downloads” folders). The path can also be a single
song or an archive. Beets supports \fIzip\fP and \fItar\fP archives out of the
box. To extract \fIrar\fP files, install the \fI\%rarfile\fP package and the
\fIunrar\fP command.
.sp
Optional command flags:
.INDENT 0.0
.IP \(bu 2
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
copying, use the \fB\-m\fP (move) option. If you’d like to leave your music
files untouched, try the \fB\-C\fP (don’t copy) and \fB\-W\fP (don’t write tags)
options. You can also disable this behavior by default in the
configuration file (below).
.IP \(bu 2
Also, you can disable the autotagging behavior entirely using \fB\-A\fP
(don’t autotag)—then your music will be imported with its existing
metadata.
.IP \(bu 2
During a long tagging import, it can be useful to keep track of albums
that weren’t tagged successfully—either because they’re not in the
MusicBrainz database or because something’s wrong with the files. Use the
\fB\-l\fP option to specify a filename to log every time you skip an album
or import it “as\-is” or an album gets skipped as a duplicate.
.IP \(bu 2
Relatedly, the \fB\-q\fP (quiet) option can help with large imports by
autotagging without ever bothering to ask for user input. Whenever the
normal autotagger mode would ask for confirmation, the quiet mode
pessimistically skips the album. The quiet mode also disables the tagger’s
ability to resume interrupted imports.
.IP \(bu 2
Speaking of resuming interrupted imports, the tagger will prompt you if it
seems like the last import of the directory was interrupted (by you or by
a crash). If you want to skip this prompt, you can say “yes” automatically
by providing \fB\-p\fP or “no” using \fB\-P\fP\&. The resuming feature can be
disabled by default using a configuration option (see below).
.IP \(bu 2
If you want to import only the \fInew\fP stuff from a directory, use the
\fB\-i\fP
option to run an \fIincremental\fP import. With this flag, beets will keep
track of every directory it ever imports and avoid importing them again.
This is useful if you have an “incoming” directory that you periodically
add things to.
To get this to work correctly, you’ll need to use an incremental import \fIevery
time\fP you run an import on the directory in question—including the first
time, when no subdirectories will be skipped. So consider enabling the
\fBincremental\fP configuration option.
.IP \(bu 2
When beets applies metadata to your music, it will retain the value of any
existing tags that weren’t overwritten, and import them into the database. You
may prefer to only use existing metadata for finding matches, and to erase it
completely when new metadata is applied. You can enforce this behavior with
the \fB\-\-from\-scratch\fP option, or the \fBfrom_scratch\fP configuration option.
.IP \(bu 2
By default, beets will proceed without asking if it finds a very close
metadata match. To disable this and have the importer ask you every time,
use the \fB\-t\fP (for \fItimid\fP) option.
.IP \(bu 2
The importer typically works in a whole\-album\-at\-a\-time mode. If you
instead want to import individual, non\-album tracks, use the \fIsingleton\fP
mode by supplying the \fB\-s\fP option.
.IP \(bu 2
If you have an album that’s split across several directories under a common
top directory, use the \fB\-\-flat\fP option. This takes all the music files
under the directory (recursively) and treats them as a single large album
instead of as one album per directory. This can help with your more stubborn
multi\-disc albums.
.IP \(bu 2
Similarly, if you have one directory that contains multiple albums, use the
\fB\-\-group\-albums\fP option to split the files based on their metadata before
matching them as separate albums.
.IP \(bu 2
If you want to preview which files would be imported, use the \fB\-\-pretend\fP
option. If set, beets will just print a list of files that it would
otherwise import.
.IP \(bu 2
If you already have a metadata backend ID that matches the items to be
imported, you can instruct beets to restrict the search to that ID instead of
searching for other candidates by using the \fB\-\-search\-id SEARCH_ID\fP option.
Multiple IDs can be specified by simply repeating the option several times.
.IP \(bu 2
You can supply \fB\-\-set field=value\fP to assign \fIfield\fP to \fIvalue\fP on import.
These assignments will merge with (and possibly override) the
set_fields configuration dictionary. You can use the option multiple
times on the command line, like so:
.INDENT 2.0
.INDENT 3.5
.sp
.nf
.ft C
beet import \-\-set genre="Alternative Rock" \-\-set mood="emotional"
.ft P
.fi
.UNINDENT
.UNINDENT
.UNINDENT
.SS list
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
beet list [\-apf] QUERY
.ft P
.fi
.UNINDENT
.UNINDENT
.sp
Queries the database for music.
.sp
Want to search for “Gronlandic Edit” by of Montreal? Try \fBbeet list
gronlandic\fP\&. Maybe you want to see everything released in 2009 with
“vegetables” in the title? Try \fBbeet list year:2009 title:vegetables\fP\&. You
can also specify the sort order. (Read more in query\&.)
.sp
You can use the \fB\-a\fP switch to search for albums instead of individual items.
In this case, the queries you use are restricted to album\-level fields: for
example, you can search for \fByear:1969\fP but query parts for item\-level fields
like \fBtitle:foo\fP will be ignored. Remember that \fBartist\fP is an item\-level
field; \fBalbumartist\fP is the corresponding album field.
.sp
The \fB\-p\fP option makes beets print out filenames of matched items, which might
be useful for piping into other Unix commands (such as \fI\%xargs\fP). Similarly, the
\fB\-f\fP option lets you specify a specific format with which to print every album
or track. This uses the same template syntax as beets’ path formats\&. For example, the command \fBbeet ls \-af \(aq$album: $tracktotal\(aq
beatles\fP prints out the number of tracks on each Beatles album. In Unix shells,
remember to enclose the template argument in single quotes to avoid environment
variable expansion.
.SS remove
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
beet remove [\-adf] QUERY
.ft P
.fi
.UNINDENT
.UNINDENT
.sp
Remove music from your library.
.sp
This command uses the same query syntax as the \fBlist\fP command.
You’ll be shown a list of the files that will be removed and asked to confirm.
By default, this just removes entries from the library database; it doesn’t
touch the files on disk. To actually delete the files, use \fBbeet remove \-d\fP\&.
If you do not want to be prompted to remove the files, use \fBbeet remove \-f\fP\&.
.SS modify
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
beet modify [\-MWay] [\-f FORMAT] QUERY [FIELD=VALUE...] [FIELD!...]
.ft P
.fi
.UNINDENT
.UNINDENT
.sp
Change the metadata for items or albums in the database.
.sp
Supply a query matching the things you want to change and a
series of \fBfield=value\fP pairs. For example, \fBbeet modify genius of love
artist="Tom Tom Club"\fP will change the artist for the track “Genius of Love.”
To remove fields (which is only possible for flexible attributes), follow a
field name with an exclamation point: \fBfield!\fP\&.
.sp
The \fB\-a\fP switch operates on albums instead of individual tracks. Without
this flag, the command will only change \fItrack\-level\fP data, even if all the
tracks belong to the same album. If you want to change an \fIalbum\-level\fP field,
such as \fByear\fP or \fBalbumartist\fP, you’ll want to use the \fB\-a\fP flag to
avoid a confusing situation where the data for individual tracks conflicts
with the data for the whole album.
.sp
Items will automatically be moved around when necessary if they’re in your
library directory, but you can disable that with \fB\-M\fP\&. Tags will be written
to the files according to the settings you have for imports, but these can be
overridden with \fB\-w\fP (write tags, the default) and \fB\-W\fP (don’t write
tags).
.sp
When you run the \fBmodify\fP command, it prints a list of all
affected items in the library and asks for your permission before making any
changes. You can then choose to abort the change (type \fIn\fP), confirm
(\fIy\fP), or interactively choose some of the items (\fIs\fP). In the latter case,
the command will prompt you for every matching item or album and invite you to
type \fIy\fP or \fIn\fP\&. This option lets you choose precisely which data to change
without spending too much time to carefully craft a query. To skip the prompts
entirely, use the \fB\-y\fP option.
.SS move
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
beet move [\-capt] [\-d DIR] QUERY
.ft P
.fi
.UNINDENT
.UNINDENT
.sp
Move or copy items in your library.
.sp
This command, by default, acts as a library consolidator: items matching the
query are renamed into your library directory structure. By specifying a
destination directory with \fB\-d\fP manually, you can move items matching a query
anywhere in your filesystem. The \fB\-c\fP option copies files instead of moving
them. As with other commands, the \fB\-a\fP option matches albums instead of items.
The \fB\-e\fP flag (for “export”) copies files without changing the database.
.sp
To perform a “dry run”, just use the \fB\-p\fP (for “pretend”) flag. This will
show you a list of files that would be moved but won’t actually change anything
on disk. The \fB\-t\fP option sets the timid mode which will ask again
before really moving or copying the files.
.SS update
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
beet update [\-F] FIELD [\-aM] QUERY
.ft P
.fi
.UNINDENT
.UNINDENT
.sp
Update the library (and, optionally, move files) to reflect out\-of\-band metadata
changes and file deletions.
.sp
This will scan all the matched files and read their tags, populating the
database with the new values. By default, files will be renamed according to
their new metadata; disable this with \fB\-M\fP\&. Beets will skip files if their
modification times have not changed, so any out\-of\-band metadata changes must
also update these for \fBbeet update\fP to recognise that the files have been
edited.
.sp
To perform a “dry run” of an update, just use the \fB\-p\fP (for “pretend”) flag.
This will show you all the proposed changes but won’t actually change anything
on disk.
.sp
By default, all the changed metadata will be populated back to the database.
If you only want certain fields to be written, specify them with the \fB\(ga\-F\(ga\fP
flags (which can be used multiple times). For the list of supported fields,
please see \fB\(gabeet fields\(ga\fP\&.
.sp
When an updated track is part of an album, the album\-level fields of \fIall\fP
tracks from the album are also updated. (Specifically, the command copies
album\-level data from the first track on the album and applies it to the
rest of the tracks.) This means that, if album\-level fields aren’t identical
within an album, some changes shown by the \fBupdate\fP command may be
overridden by data from other tracks on the same album. This means that
running the \fBupdate\fP command multiple times may show the same changes being
applied.
.SS write
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
beet write [\-pf] [QUERY]
.ft P
.fi
.UNINDENT
.UNINDENT
.sp
Write metadata from the database into files’ tags.
.sp
When you make changes to the metadata stored in beets’ library database
(during import or with the \fI\%modify\fP command, for example), you often
have the option of storing changes only in the database, leaving your files
untouched. The \fBwrite\fP command lets you later change your mind and write the
contents of the database into the files. By default, this writes the changes only if there is a difference between the database and the tags in the file.
.sp
You can think of this command as the opposite of \fI\%update\fP\&.
.sp
The \fB\-p\fP option previews metadata changes without actually applying them.
.sp
The \fB\-f\fP option forces a write to the file, even if the file tags match the database. This is useful for making sure that enabled plugins that run on write (e.g., the Scrub and Zero plugins) are run on the file.
.SS stats
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
beet stats [\-e] [QUERY]
.ft P
.fi
.UNINDENT
.UNINDENT
.sp
Show some statistics on your entire library (if you don’t provide a
query) or the matched items (if you do).
.sp
By default, the command calculates file sizes using their bitrate and
duration. The \fB\-e\fP (\fB\-\-exact\fP) option reads the exact sizes of each file
(but is slower). The exact mode also outputs the exact duration in seconds.
.SS fields
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
beet fields
.ft P
.fi
.UNINDENT
.UNINDENT
.sp
Show the item and album metadata fields available for use in query and
pathformat\&. The listing includes any template fields provided by
plugins and any flexible attributes you’ve manually assigned to your items and
albums.
.SS config
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
beet config [\-pdc]
beet config \-e
.ft P
.fi
.UNINDENT
.UNINDENT
.sp
Show or edit the user configuration. This command does one of three things:
.INDENT 0.0
.IP \(bu 2
With no options, print a YAML representation of the current user
configuration. With the \fB\-\-default\fP option, beets’ default options are
also included in the dump.
.IP \(bu 2
The \fB\-\-path\fP option instead shows the path to your configuration file.
This can be combined with the \fB\-\-default\fP flag to show where beets keeps
its internal defaults.
.IP \(bu 2
By default, sensitive information like passwords is removed when dumping the
configuration. The \fB\-\-clear\fP option includes this sensitive data.
.IP \(bu 2
With the \fB\-\-edit\fP option, beets attempts to open your config file for
editing. It first tries the \fB$EDITOR\fP environment variable and then a
fallback option depending on your platform: \fBopen\fP on OS X, \fBxdg\-open\fP
on Unix, and direct invocation on Windows.
.UNINDENT
.SH GLOBAL FLAGS
.sp
Beets has a few “global” flags that affect all commands. These must appear
between the executable name (\fBbeet\fP) and the command—for example, \fBbeet \-v
import ...\fP\&.
.INDENT 0.0
.IP \(bu 2
\fB\-l LIBPATH\fP: specify the library database file to use.
.IP \(bu 2
\fB\-d DIRECTORY\fP: specify the library root directory.
.IP \(bu 2
\fB\-v\fP: verbose mode; prints out a deluge of debugging information. Please use
this flag when reporting bugs. You can use it twice, as in \fB\-vv\fP, to make
beets even more verbose.
.IP \(bu 2
\fB\-c FILE\fP: read a specified YAML configuration file\&. This
configuration works as an overlay: rather than replacing your normal
configuration options entirely, the two are merged. Any individual options set
in this config file will override the corresponding settings in your base
configuration.
.UNINDENT
.sp
Beets also uses the \fBBEETSDIR\fP environment variable to look for
configuration and data.
.SH SHELL COMPLETION
.sp
Beets includes support for shell command completion. The command \fBbeet
completion\fP prints out a \fI\%bash\fP 3.2 script; to enable completion put a line
like this into your \fB\&.bashrc\fP or similar file:
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
eval "$(beet completion)"
.ft P
.fi
.UNINDENT
.UNINDENT
.sp
Or, to avoid slowing down your shell startup time, you can pipe the \fBbeet
completion\fP output to a file and source that instead.
.sp
You will also need to source the \fI\%bash\-completion\fP script, which is probably
available via your package manager. On OS X, you can install it via Homebrew
with \fBbrew install bash\-completion\fP; Homebrew will give you instructions for
sourcing the script.
.sp
The completion script suggests names of subcommands and (after typing
\fB\-\fP) options of the given command. If you are using a command that
accepts a query, the script will also complete field names.
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
beet list ar[TAB]
# artist: artist_credit: artist_sort: artpath:
beet list artp[TAB]
beet list artpath\e:
.ft P
.fi
.UNINDENT
.UNINDENT
.sp
(Don’t worry about the slash in front of the colon: this is a escape
sequence for the shell and won’t be seen by beets.)
.sp
Completion of plugin commands only works for those plugins
that were enabled when running \fBbeet completion\fP\&. If you add a plugin
later on you will want to re\-generate the script.
.SS zsh
.sp
If you use zsh, take a look at the included \fI\%completion script\fP\&. The script
should be placed in a directory that is part of your \fBfpath\fP, and \fInot\fP
sourced in your \fB\&.zshrc\fP\&. Running \fBecho $fpath\fP will give you a list of
valid directories.
.sp
Another approach is to use zsh’s bash completion compatibility. This snippet
defines some bash\-specific functions to make this work without errors:
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
autoload bashcompinit
bashcompinit
_get_comp_words_by_ref() { :; }
compopt() { :; }
_filedir() { :; }
eval "$(beet completion)"
.ft P
.fi
.UNINDENT
.UNINDENT
.SH SEE ALSO
.sp
\fBhttp://beets.readthedocs.org/\fP
.sp
\fBbeetsconfig(5)\fP
.SH AUTHOR
Adrian Sampson
.SH COPYRIGHT
2016, Adrian Sampson
.\" Generated by docutils manpage writer.
.
beets-1.4.6/PKG-INFO 0000644 0000765 0000024 00000012715 13216774613 014705 0 ustar asampson staff 0000000 0000000 Metadata-Version: 1.1
Name: beets
Version: 1.4.6
Summary: music tagger and library organizer
Home-page: http://beets.io/
Author: Adrian Sampson
Author-email: adrian@radbox.org
License: MIT
Description-Content-Type: UNKNOWN
Description: .. image:: http://img.shields.io/pypi/v/beets.svg
:target: https://pypi.python.org/pypi/beets
.. image:: http://img.shields.io/codecov/c/github/beetbox/beets.svg
:target: https://codecov.io/github/beetbox/beets
.. image:: https://travis-ci.org/beetbox/beets.svg?branch=master
:target: https://travis-ci.org/beetbox/beets
Beets is the media library management system for obsessive-compulsive music
geeks.
The purpose of beets is to get your music collection right once and for all.
It catalogs your collection, automatically improving its metadata as it goes.
It then provides a bouquet of tools for manipulating and accessing your music.
Here's an example of beets' brainy tag corrector doing its thing::
$ beet import ~/music/ladytron
Tagging:
Ladytron - Witching Hour
(Similarity: 98.4%)
* Last One Standing -> The Last One Standing
* Beauty -> Beauty*2
* White Light Generation -> Whitelightgenerator
* All the Way -> All the Way...
Because beets is designed as a library, it can do almost anything you can
imagine for your music collection. Via `plugins`_, beets becomes a panacea:
- Fetch or calculate all the metadata you could possibly need: `album art`_,
`lyrics`_, `genres`_, `tempos`_, `ReplayGain`_ levels, or `acoustic
fingerprints`_.
- Get metadata from `MusicBrainz`_, `Discogs`_, and `Beatport`_. Or guess
metadata using songs' filenames or their acoustic fingerprints.
- `Transcode audio`_ to any format you like.
- Check your library for `duplicate tracks and albums`_ or for `albums that
are missing tracks`_.
- Clean up crufty tags left behind by other, less-awesome tools.
- Embed and extract album art from files' metadata.
- Browse your music library graphically through a Web browser and play it in any
browser that supports `HTML5 Audio`_.
- Analyze music files' metadata from the command line.
- Listen to your library with a music player that speaks the `MPD`_ protocol
and works with a staggering variety of interfaces.
If beets doesn't do what you want yet, `writing your own plugin`_ is
shockingly simple if you know a little Python.
.. _plugins: http://beets.readthedocs.org/page/plugins/
.. _MPD: http://www.musicpd.org/
.. _MusicBrainz music collection: http://musicbrainz.org/doc/Collections/
.. _writing your own plugin:
http://beets.readthedocs.org/page/dev/plugins.html
.. _HTML5 Audio:
http://www.w3.org/TR/html-markup/audio.html
.. _albums that are missing tracks:
http://beets.readthedocs.org/page/plugins/missing.html
.. _duplicate tracks and albums:
http://beets.readthedocs.org/page/plugins/duplicates.html
.. _Transcode audio:
http://beets.readthedocs.org/page/plugins/convert.html
.. _Discogs: http://www.discogs.com/
.. _acoustic fingerprints:
http://beets.readthedocs.org/page/plugins/chroma.html
.. _ReplayGain: http://beets.readthedocs.org/page/plugins/replaygain.html
.. _tempos: http://beets.readthedocs.org/page/plugins/acousticbrainz.html
.. _genres: http://beets.readthedocs.org/page/plugins/lastgenre.html
.. _album art: http://beets.readthedocs.org/page/plugins/fetchart.html
.. _lyrics: http://beets.readthedocs.org/page/plugins/lyrics.html
.. _MusicBrainz: http://musicbrainz.org/
.. _Beatport: https://www.beatport.com
Read More
---------
Learn more about beets at `its Web site`_. Follow `@b33ts`_ on Twitter for
news and updates.
You can install beets by typing ``pip install beets``. Then check out the
`Getting Started`_ guide.
.. _its Web site: http://beets.io/
.. _Getting Started: http://beets.readthedocs.org/page/guides/main.html
.. _@b33ts: http://twitter.com/b33ts/
Authors
-------
Beets is by `Adrian Sampson`_ with a supporting cast of thousands. For help,
please contact the `mailing list`_.
.. _mailing list: https://groups.google.com/forum/#!forum/beets-users
.. _Adrian Sampson: http://homes.cs.washington.edu/~asampson/
Platform: ALL
Classifier: Topic :: Multimedia :: Sound/Audio
Classifier: Topic :: Multimedia :: Sound/Audio :: Players :: MP3
Classifier: License :: OSI Approved :: MIT License
Classifier: Environment :: Console
Classifier: Environment :: Web Environment
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.4
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: Implementation :: CPython
beets-1.4.6/LICENSE 0000644 0000765 0000024 00000002070 13025125202 014564 0 ustar asampson staff 0000000 0000000 The MIT License
Copyright (c) 2010-2016 Adrian Sampson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
beets-1.4.6/test/ 0000755 0000765 0000024 00000000000 13216774613 014561 5 ustar asampson staff 0000000 0000000 beets-1.4.6/test/test_ui_importer.py 0000644 0000765 0000024 00000011353 13025125203 020512 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Tests the TerminalImportSession. The tests are the same as in the
test_importer module. But here the test importer inherits from
``TerminalImportSession``. So we test this class, too.
"""
from __future__ import division, absolute_import, print_function
import unittest
from test._common import DummyIO
from test import test_importer
from beets.ui.commands import TerminalImportSession
from beets import importer
from beets import config
import six
class TestTerminalImportSession(TerminalImportSession):
def __init__(self, *args, **kwargs):
self.io = kwargs.pop('io')
super(TestTerminalImportSession, self).__init__(*args, **kwargs)
self._choices = []
default_choice = importer.action.APPLY
def add_choice(self, choice):
self._choices.append(choice)
def clear_choices(self):
self._choices = []
def choose_match(self, task):
self._add_choice_input()
return super(TestTerminalImportSession, self).choose_match(task)
def choose_item(self, task):
self._add_choice_input()
return super(TestTerminalImportSession, self).choose_item(task)
def _add_choice_input(self):
try:
choice = self._choices.pop(0)
except IndexError:
choice = self.default_choice
if choice == importer.action.APPLY:
self.io.addinput(u'A')
elif choice == importer.action.ASIS:
self.io.addinput(u'U')
elif choice == importer.action.ALBUMS:
self.io.addinput(u'G')
elif choice == importer.action.TRACKS:
self.io.addinput(u'T')
elif choice == importer.action.SKIP:
self.io.addinput(u'S')
elif isinstance(choice, int):
self.io.addinput(u'M')
self.io.addinput(six.text_type(choice))
self._add_choice_input()
else:
raise Exception(u'Unknown choice %s' % choice)
class TerminalImportSessionSetup(object):
"""Overwrites test_importer.ImportHelper to provide a terminal importer
"""
def _setup_import_session(self, import_dir=None, delete=False,
threaded=False, copy=True, singletons=False,
move=False, autotag=True):
config['import']['copy'] = copy
config['import']['delete'] = delete
config['import']['timid'] = True
config['threaded'] = False
config['import']['singletons'] = singletons
config['import']['move'] = move
config['import']['autotag'] = autotag
config['import']['resume'] = False
if not hasattr(self, 'io'):
self.io = DummyIO()
self.io.install()
self.importer = TestTerminalImportSession(
self.lib, loghandler=None, query=None, io=self.io,
paths=[import_dir or self.import_dir],
)
class NonAutotaggedImportTest(TerminalImportSessionSetup,
test_importer.NonAutotaggedImportTest):
pass
class ImportTest(TerminalImportSessionSetup,
test_importer.ImportTest):
pass
class ImportSingletonTest(TerminalImportSessionSetup,
test_importer.ImportSingletonTest):
pass
class ImportTracksTest(TerminalImportSessionSetup,
test_importer.ImportTracksTest):
pass
class ImportCompilationTest(TerminalImportSessionSetup,
test_importer.ImportCompilationTest):
pass
class ImportExistingTest(TerminalImportSessionSetup,
test_importer.ImportExistingTest):
pass
class ChooseCandidateTest(TerminalImportSessionSetup,
test_importer.ChooseCandidateTest):
pass
class GroupAlbumsImportTest(TerminalImportSessionSetup,
test_importer.GroupAlbumsImportTest):
pass
class GlobalGroupAlbumsImportTest(TerminalImportSessionSetup,
test_importer.GlobalGroupAlbumsImportTest):
pass
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/test_discogs.py 0000644 0000765 0000024 00000035073 13032602010 017606 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Tests for discogs plugin.
"""
from __future__ import division, absolute_import, print_function
import unittest
from test import _common
from test._common import Bag
from test.helper import capture_log
from beetsplug.discogs import DiscogsPlugin
class DGAlbumInfoTest(_common.TestCase):
def _make_release(self, tracks=None):
"""Returns a Bag that mimics a discogs_client.Release. The list
of elements on the returned Bag is incomplete, including just
those required for the tests on this class."""
data = {
'id': 'ALBUM ID',
'uri': 'ALBUM URI',
'title': 'ALBUM TITLE',
'year': '3001',
'artists': [{
'name': 'ARTIST NAME',
'id': 'ARTIST ID',
'join': ','
}],
'formats': [{
'descriptions': ['FORMAT DESC 1', 'FORMAT DESC 2'],
'name': 'FORMAT',
'qty': 1
}],
'labels': [{
'name': 'LABEL NAME',
'catno': 'CATALOG NUMBER',
}],
'tracklist': []
}
if tracks:
for recording in tracks:
data['tracklist'].append(recording)
return Bag(data=data,
# Make some fields available as properties, as they are
# accessed by DiscogsPlugin methods.
title=data['title'],
artists=[Bag(data=d) for d in data['artists']])
def _make_track(self, title, position='', duration='', type_=None):
track = {
'title': title,
'position': position,
'duration': duration
}
if type_ is not None:
# Test samples on discogs_client do not have a 'type_' field, but
# the API seems to return it. Values: 'track' for regular tracks,
# 'heading' for descriptive texts (ie. not real tracks - 12.13.2).
track['type_'] = type_
return track
def _make_release_from_positions(self, positions):
"""Return a Bag that mimics a discogs_client.Release with a
tracklist where tracks have the specified `positions`."""
tracks = [self._make_track('TITLE%s' % i, position) for
(i, position) in enumerate(positions, start=1)]
return self._make_release(tracks)
def test_parse_media_for_tracks(self):
tracks = [self._make_track('TITLE ONE', '1', '01:01'),
self._make_track('TITLE TWO', '2', '02:02')]
release = self._make_release(tracks=tracks)
d = DiscogsPlugin().get_album_info(release)
t = d.tracks
self.assertEqual(d.media, 'FORMAT')
self.assertEqual(t[0].media, d.media)
self.assertEqual(t[1].media, d.media)
def test_parse_medium_numbers_single_medium(self):
release = self._make_release_from_positions(['1', '2'])
d = DiscogsPlugin().get_album_info(release)
t = d.tracks
self.assertEqual(d.mediums, 1)
self.assertEqual(t[0].medium, 1)
self.assertEqual(t[0].medium_total, 1)
self.assertEqual(t[1].medium, 1)
self.assertEqual(t[0].medium_total, 1)
def test_parse_medium_numbers_two_mediums(self):
release = self._make_release_from_positions(['1-1', '2-1'])
d = DiscogsPlugin().get_album_info(release)
t = d.tracks
self.assertEqual(d.mediums, 2)
self.assertEqual(t[0].medium, 1)
self.assertEqual(t[0].medium_total, 2)
self.assertEqual(t[1].medium, 2)
self.assertEqual(t[1].medium_total, 2)
def test_parse_medium_numbers_two_mediums_two_sided(self):
release = self._make_release_from_positions(['A1', 'B1', 'C1'])
d = DiscogsPlugin().get_album_info(release)
t = d.tracks
self.assertEqual(d.mediums, 2)
self.assertEqual(t[0].medium, 1)
self.assertEqual(t[0].medium_total, 2)
self.assertEqual(t[0].medium_index, 1)
self.assertEqual(t[1].medium, 1)
self.assertEqual(t[1].medium_total, 2)
self.assertEqual(t[1].medium_index, 2)
self.assertEqual(t[2].medium, 2)
self.assertEqual(t[2].medium_total, 2)
self.assertEqual(t[2].medium_index, 1)
def test_parse_track_indices(self):
release = self._make_release_from_positions(['1', '2'])
d = DiscogsPlugin().get_album_info(release)
t = d.tracks
self.assertEqual(t[0].medium_index, 1)
self.assertEqual(t[0].index, 1)
self.assertEqual(t[0].medium_total, 1)
self.assertEqual(t[1].medium_index, 2)
self.assertEqual(t[1].index, 2)
self.assertEqual(t[1].medium_total, 1)
def test_parse_track_indices_several_media(self):
release = self._make_release_from_positions(['1-1', '1-2', '2-1',
'3-1'])
d = DiscogsPlugin().get_album_info(release)
t = d.tracks
self.assertEqual(d.mediums, 3)
self.assertEqual(t[0].medium_index, 1)
self.assertEqual(t[0].index, 1)
self.assertEqual(t[0].medium_total, 3)
self.assertEqual(t[1].medium_index, 2)
self.assertEqual(t[1].index, 2)
self.assertEqual(t[1].medium_total, 3)
self.assertEqual(t[2].medium_index, 1)
self.assertEqual(t[2].index, 3)
self.assertEqual(t[2].medium_total, 3)
self.assertEqual(t[3].medium_index, 1)
self.assertEqual(t[3].index, 4)
self.assertEqual(t[3].medium_total, 3)
def test_parse_position(self):
"""Test the conversion of discogs `position` to medium, medium_index
and subtrack_index."""
# List of tuples (discogs_position, (medium, medium_index, subindex)
positions = [('1', (None, '1', None)),
('A12', ('A', '12', None)),
('12-34', ('12-', '34', None)),
('CD1-1', ('CD1-', '1', None)),
('1.12', (None, '1', '12')),
('12.a', (None, '12', 'A')),
('12.34', (None, '12', '34')),
('1ab', (None, '1', 'AB')),
# Non-standard
('IV', ('IV', None, None)),
]
d = DiscogsPlugin()
for position, expected in positions:
self.assertEqual(d.get_track_index(position), expected)
def test_parse_tracklist_without_sides(self):
"""Test standard Discogs position 12.2.9#1: "without sides"."""
release = self._make_release_from_positions(['1', '2', '3'])
d = DiscogsPlugin().get_album_info(release)
self.assertEqual(d.mediums, 1)
self.assertEqual(len(d.tracks), 3)
def test_parse_tracklist_with_sides(self):
"""Test standard Discogs position 12.2.9#2: "with sides"."""
release = self._make_release_from_positions(['A1', 'A2', 'B1', 'B2'])
d = DiscogsPlugin().get_album_info(release)
self.assertEqual(d.mediums, 1) # 2 sides = 1 LP
self.assertEqual(len(d.tracks), 4)
def test_parse_tracklist_multiple_lp(self):
"""Test standard Discogs position 12.2.9#3: "multiple LP"."""
release = self._make_release_from_positions(['A1', 'A2', 'B1', 'C1'])
d = DiscogsPlugin().get_album_info(release)
self.assertEqual(d.mediums, 2) # 3 sides = 1 LP + 1 LP
self.assertEqual(len(d.tracks), 4)
def test_parse_tracklist_multiple_cd(self):
"""Test standard Discogs position 12.2.9#4: "multiple CDs"."""
release = self._make_release_from_positions(['1-1', '1-2', '2-1',
'3-1'])
d = DiscogsPlugin().get_album_info(release)
self.assertEqual(d.mediums, 3)
self.assertEqual(len(d.tracks), 4)
def test_parse_tracklist_non_standard(self):
"""Test non standard Discogs position."""
release = self._make_release_from_positions(['I', 'II', 'III', 'IV'])
d = DiscogsPlugin().get_album_info(release)
self.assertEqual(d.mediums, 1)
self.assertEqual(len(d.tracks), 4)
def test_parse_tracklist_subtracks_dot(self):
"""Test standard Discogs position 12.2.9#5: "sub tracks, dots"."""
release = self._make_release_from_positions(['1', '2.1', '2.2', '3'])
d = DiscogsPlugin().get_album_info(release)
self.assertEqual(d.mediums, 1)
self.assertEqual(len(d.tracks), 3)
release = self._make_release_from_positions(['A1', 'A2.1', 'A2.2',
'A3'])
d = DiscogsPlugin().get_album_info(release)
self.assertEqual(d.mediums, 1)
self.assertEqual(len(d.tracks), 3)
def test_parse_tracklist_subtracks_letter(self):
"""Test standard Discogs position 12.2.9#5: "sub tracks, letter"."""
release = self._make_release_from_positions(['A1', 'A2a', 'A2b', 'A3'])
d = DiscogsPlugin().get_album_info(release)
self.assertEqual(d.mediums, 1)
self.assertEqual(len(d.tracks), 3)
release = self._make_release_from_positions(['A1', 'A2.a', 'A2.b',
'A3'])
d = DiscogsPlugin().get_album_info(release)
self.assertEqual(d.mediums, 1)
self.assertEqual(len(d.tracks), 3)
def test_parse_tracklist_subtracks_extra_material(self):
"""Test standard Discogs position 12.2.9#6: "extra material"."""
release = self._make_release_from_positions(['1', '2', 'Video 1'])
d = DiscogsPlugin().get_album_info(release)
self.assertEqual(d.mediums, 2)
self.assertEqual(len(d.tracks), 3)
def test_parse_tracklist_subtracks_indices(self):
"""Test parsing of subtracks that include index tracks."""
release = self._make_release_from_positions(['', '', '1.1', '1.2'])
# Track 1: Index track with medium title
release.data['tracklist'][0]['title'] = 'MEDIUM TITLE'
# Track 2: Index track with track group title
release.data['tracklist'][1]['title'] = 'TRACK GROUP TITLE'
d = DiscogsPlugin().get_album_info(release)
self.assertEqual(d.mediums, 1)
self.assertEqual(d.tracks[0].disctitle, 'MEDIUM TITLE')
self.assertEqual(len(d.tracks), 1)
self.assertEqual(d.tracks[0].title, 'TRACK GROUP TITLE')
def test_parse_tracklist_subtracks_nested_logical(self):
"""Test parsing of subtracks defined inside a index track that are
logical subtracks (ie. should be grouped together into a single track).
"""
release = self._make_release_from_positions(['1', '', '3'])
# Track 2: Index track with track group title, and sub_tracks
release.data['tracklist'][1]['title'] = 'TRACK GROUP TITLE'
release.data['tracklist'][1]['sub_tracks'] = [
self._make_track('TITLE ONE', '2.1', '01:01'),
self._make_track('TITLE TWO', '2.2', '02:02')
]
d = DiscogsPlugin().get_album_info(release)
self.assertEqual(d.mediums, 1)
self.assertEqual(len(d.tracks), 3)
self.assertEqual(d.tracks[1].title, 'TRACK GROUP TITLE')
def test_parse_tracklist_subtracks_nested_physical(self):
"""Test parsing of subtracks defined inside a index track that are
physical subtracks (ie. should not be grouped together).
"""
release = self._make_release_from_positions(['1', '', '4'])
# Track 2: Index track with track group title, and sub_tracks
release.data['tracklist'][1]['title'] = 'TRACK GROUP TITLE'
release.data['tracklist'][1]['sub_tracks'] = [
self._make_track('TITLE ONE', '2', '01:01'),
self._make_track('TITLE TWO', '3', '02:02')
]
d = DiscogsPlugin().get_album_info(release)
self.assertEqual(d.mediums, 1)
self.assertEqual(len(d.tracks), 4)
self.assertEqual(d.tracks[1].title, 'TITLE ONE')
self.assertEqual(d.tracks[2].title, 'TITLE TWO')
def test_parse_tracklist_disctitles(self):
"""Test parsing of index tracks that act as disc titles."""
release = self._make_release_from_positions(['', '1-1', '1-2', '',
'2-1'])
# Track 1: Index track with medium title (Cd1)
release.data['tracklist'][0]['title'] = 'MEDIUM TITLE CD1'
# Track 4: Index track with medium title (Cd2)
release.data['tracklist'][3]['title'] = 'MEDIUM TITLE CD2'
d = DiscogsPlugin().get_album_info(release)
self.assertEqual(d.mediums, 2)
self.assertEqual(d.tracks[0].disctitle, 'MEDIUM TITLE CD1')
self.assertEqual(d.tracks[1].disctitle, 'MEDIUM TITLE CD1')
self.assertEqual(d.tracks[2].disctitle, 'MEDIUM TITLE CD2')
self.assertEqual(len(d.tracks), 3)
def test_parse_minimal_release(self):
"""Test parsing of a release with the minimal amount of information."""
data = {'id': 123,
'tracklist': [self._make_track('A', '1', '01:01')],
'artists': [{'name': 'ARTIST NAME', 'id': 321, 'join': ''}],
'title': 'TITLE'}
release = Bag(data=data,
title=data['title'],
artists=[Bag(data=d) for d in data['artists']])
d = DiscogsPlugin().get_album_info(release)
self.assertEqual(d.artist, 'ARTIST NAME')
self.assertEqual(d.album, 'TITLE')
self.assertEqual(len(d.tracks), 1)
def test_parse_release_without_required_fields(self):
"""Test parsing of a release that does not have the required fields."""
release = Bag(data={}, refresh=lambda *args: None)
with capture_log() as logs:
d = DiscogsPlugin().get_album_info(release)
self.assertEqual(d, None)
self.assertIn('Release does not contain the required fields', logs[0])
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/test_library.py 0000644 0000765 0000024 00000126614 13040564606 017642 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Tests for non-query database functions of Item.
"""
from __future__ import division, absolute_import, print_function
import os
import os.path
import stat
import shutil
import re
import unicodedata
import sys
import time
import unittest
from test import _common
from test._common import item
import beets.library
import beets.mediafile
import beets.dbcore.query
from beets import util
from beets import plugins
from beets import config
from beets.mediafile import MediaFile
from beets.util import syspath, bytestring_path
from test.helper import TestHelper
import six
# Shortcut to path normalization.
np = util.normpath
class LoadTest(_common.LibTestCase):
def test_load_restores_data_from_db(self):
original_title = self.i.title
self.i.title = u'something'
self.i.load()
self.assertEqual(original_title, self.i.title)
def test_load_clears_dirty_flags(self):
self.i.artist = u'something'
self.assertTrue('artist' in self.i._dirty)
self.i.load()
self.assertTrue('artist' not in self.i._dirty)
class StoreTest(_common.LibTestCase):
def test_store_changes_database_value(self):
self.i.year = 1987
self.i.store()
new_year = self.lib._connection().execute(
'select year from items where '
'title="the title"').fetchone()['year']
self.assertEqual(new_year, 1987)
def test_store_only_writes_dirty_fields(self):
original_genre = self.i.genre
self.i._values_fixed['genre'] = u'beatboxing' # change w/o dirtying
self.i.store()
new_genre = self.lib._connection().execute(
'select genre from items where '
'title="the title"').fetchone()['genre']
self.assertEqual(new_genre, original_genre)
def test_store_clears_dirty_flags(self):
self.i.composer = u'tvp'
self.i.store()
self.assertTrue('composer' not in self.i._dirty)
class AddTest(_common.TestCase):
def setUp(self):
super(AddTest, self).setUp()
self.lib = beets.library.Library(':memory:')
self.i = item()
def test_item_add_inserts_row(self):
self.lib.add(self.i)
new_grouping = self.lib._connection().execute(
'select grouping from items '
'where composer="the composer"').fetchone()['grouping']
self.assertEqual(new_grouping, self.i.grouping)
def test_library_add_path_inserts_row(self):
i = beets.library.Item.from_path(
os.path.join(_common.RSRC, b'full.mp3')
)
self.lib.add(i)
new_grouping = self.lib._connection().execute(
'select grouping from items '
'where composer="the composer"').fetchone()['grouping']
self.assertEqual(new_grouping, self.i.grouping)
class RemoveTest(_common.LibTestCase):
def test_remove_deletes_from_db(self):
self.i.remove()
c = self.lib._connection().execute('select * from items')
self.assertEqual(c.fetchone(), None)
class GetSetTest(_common.TestCase):
def setUp(self):
super(GetSetTest, self).setUp()
self.i = item()
def test_set_changes_value(self):
self.i.bpm = 4915
self.assertEqual(self.i.bpm, 4915)
def test_set_sets_dirty_flag(self):
self.i.comp = not self.i.comp
self.assertTrue('comp' in self.i._dirty)
def test_set_does_not_dirty_if_value_unchanged(self):
self.i.title = self.i.title
self.assertTrue('title' not in self.i._dirty)
def test_invalid_field_raises_attributeerror(self):
self.assertRaises(AttributeError, getattr, self.i, u'xyzzy')
class DestinationTest(_common.TestCase):
def setUp(self):
super(DestinationTest, self).setUp()
self.lib = beets.library.Library(':memory:')
self.i = item(self.lib)
def tearDown(self):
super(DestinationTest, self).tearDown()
self.lib._connection().close()
# Reset config if it was changed in test cases
config.clear()
config.read(user=False, defaults=True)
def test_directory_works_with_trailing_slash(self):
self.lib.directory = b'one/'
self.lib.path_formats = [(u'default', u'two')]
self.assertEqual(self.i.destination(), np('one/two'))
def test_directory_works_without_trailing_slash(self):
self.lib.directory = b'one'
self.lib.path_formats = [(u'default', u'two')]
self.assertEqual(self.i.destination(), np('one/two'))
def test_destination_substitues_metadata_values(self):
self.lib.directory = b'base'
self.lib.path_formats = [(u'default', u'$album/$artist $title')]
self.i.title = 'three'
self.i.artist = 'two'
self.i.album = 'one'
self.assertEqual(self.i.destination(),
np('base/one/two three'))
def test_destination_preserves_extension(self):
self.lib.directory = b'base'
self.lib.path_formats = [(u'default', u'$title')]
self.i.path = 'hey.audioformat'
self.assertEqual(self.i.destination(),
np('base/the title.audioformat'))
def test_lower_case_extension(self):
self.lib.directory = b'base'
self.lib.path_formats = [(u'default', u'$title')]
self.i.path = 'hey.MP3'
self.assertEqual(self.i.destination(),
np('base/the title.mp3'))
def test_destination_pads_some_indices(self):
self.lib.directory = b'base'
self.lib.path_formats = [(u'default',
u'$track $tracktotal $disc $disctotal $bpm')]
self.i.track = 1
self.i.tracktotal = 2
self.i.disc = 3
self.i.disctotal = 4
self.i.bpm = 5
self.assertEqual(self.i.destination(),
np('base/01 02 03 04 5'))
def test_destination_pads_date_values(self):
self.lib.directory = b'base'
self.lib.path_formats = [(u'default', u'$year-$month-$day')]
self.i.year = 1
self.i.month = 2
self.i.day = 3
self.assertEqual(self.i.destination(),
np('base/0001-02-03'))
def test_destination_escapes_slashes(self):
self.i.album = 'one/two'
dest = self.i.destination()
self.assertTrue(b'one' in dest)
self.assertTrue(b'two' in dest)
self.assertFalse(b'one/two' in dest)
def test_destination_escapes_leading_dot(self):
self.i.album = '.something'
dest = self.i.destination()
self.assertTrue(b'something' in dest)
self.assertFalse(b'/.' in dest)
def test_destination_preserves_legitimate_slashes(self):
self.i.artist = 'one'
self.i.album = 'two'
dest = self.i.destination()
self.assertTrue(os.path.join(b'one', b'two') in dest)
def test_destination_long_names_truncated(self):
self.i.title = u'X' * 300
self.i.artist = u'Y' * 300
for c in self.i.destination().split(util.PATH_SEP):
self.assertTrue(len(c) <= 255)
def test_destination_long_names_keep_extension(self):
self.i.title = u'X' * 300
self.i.path = b'something.extn'
dest = self.i.destination()
self.assertEqual(dest[-5:], b'.extn')
def test_distination_windows_removes_both_separators(self):
self.i.title = 'one \\ two / three.mp3'
with _common.platform_windows():
p = self.i.destination()
self.assertFalse(b'one \\ two' in p)
self.assertFalse(b'one / two' in p)
self.assertFalse(b'two \\ three' in p)
self.assertFalse(b'two / three' in p)
def test_path_with_format(self):
self.lib.path_formats = [(u'default', u'$artist/$album ($format)')]
p = self.i.destination()
self.assertTrue(b'(FLAC)' in p)
def test_heterogeneous_album_gets_single_directory(self):
i1, i2 = item(), item()
self.lib.add_album([i1, i2])
i1.year, i2.year = 2009, 2010
self.lib.path_formats = [(u'default', u'$album ($year)/$track $title')]
dest1, dest2 = i1.destination(), i2.destination()
self.assertEqual(os.path.dirname(dest1), os.path.dirname(dest2))
def test_default_path_for_non_compilations(self):
self.i.comp = False
self.lib.add_album([self.i])
self.lib.directory = b'one'
self.lib.path_formats = [(u'default', u'two'),
(u'comp:true', u'three')]
self.assertEqual(self.i.destination(), np('one/two'))
def test_singleton_path(self):
i = item(self.lib)
self.lib.directory = b'one'
self.lib.path_formats = [
(u'default', u'two'),
(u'singleton:true', u'four'),
(u'comp:true', u'three'),
]
self.assertEqual(i.destination(), np('one/four'))
def test_comp_before_singleton_path(self):
i = item(self.lib)
i.comp = True
self.lib.directory = b'one'
self.lib.path_formats = [
(u'default', u'two'),
(u'comp:true', u'three'),
(u'singleton:true', u'four'),
]
self.assertEqual(i.destination(), np('one/three'))
def test_comp_path(self):
self.i.comp = True
self.lib.add_album([self.i])
self.lib.directory = b'one'
self.lib.path_formats = [
(u'default', u'two'),
(u'comp:true', u'three'),
]
self.assertEqual(self.i.destination(), np('one/three'))
def test_albumtype_query_path(self):
self.i.comp = True
self.lib.add_album([self.i])
self.i.albumtype = u'sometype'
self.lib.directory = b'one'
self.lib.path_formats = [
(u'default', u'two'),
(u'albumtype:sometype', u'four'),
(u'comp:true', u'three'),
]
self.assertEqual(self.i.destination(), np('one/four'))
def test_albumtype_path_fallback_to_comp(self):
self.i.comp = True
self.lib.add_album([self.i])
self.i.albumtype = u'sometype'
self.lib.directory = b'one'
self.lib.path_formats = [
(u'default', u'two'),
(u'albumtype:anothertype', u'four'),
(u'comp:true', u'three'),
]
self.assertEqual(self.i.destination(), np('one/three'))
def test_get_formatted_does_not_replace_separators(self):
with _common.platform_posix():
name = os.path.join('a', 'b')
self.i.title = name
newname = self.i.formatted().get('title')
self.assertEqual(name, newname)
def test_get_formatted_pads_with_zero(self):
with _common.platform_posix():
self.i.track = 1
name = self.i.formatted().get('track')
self.assertTrue(name.startswith('0'))
def test_get_formatted_uses_kbps_bitrate(self):
with _common.platform_posix():
self.i.bitrate = 12345
val = self.i.formatted().get('bitrate')
self.assertEqual(val, u'12kbps')
def test_get_formatted_uses_khz_samplerate(self):
with _common.platform_posix():
self.i.samplerate = 12345
val = self.i.formatted().get('samplerate')
self.assertEqual(val, u'12kHz')
def test_get_formatted_datetime(self):
with _common.platform_posix():
self.i.added = 1368302461.210265
val = self.i.formatted().get('added')
self.assertTrue(val.startswith('2013'))
def test_get_formatted_none(self):
with _common.platform_posix():
self.i.some_other_field = None
val = self.i.formatted().get('some_other_field')
self.assertEqual(val, u'')
def test_artist_falls_back_to_albumartist(self):
self.i.artist = u''
self.i.albumartist = u'something'
self.lib.path_formats = [(u'default', u'$artist')]
p = self.i.destination()
self.assertEqual(p.rsplit(util.PATH_SEP, 1)[1], b'something')
def test_albumartist_falls_back_to_artist(self):
self.i.artist = u'trackartist'
self.i.albumartist = u''
self.lib.path_formats = [(u'default', u'$albumartist')]
p = self.i.destination()
self.assertEqual(p.rsplit(util.PATH_SEP, 1)[1], b'trackartist')
def test_artist_overrides_albumartist(self):
self.i.artist = u'theartist'
self.i.albumartist = u'something'
self.lib.path_formats = [(u'default', u'$artist')]
p = self.i.destination()
self.assertEqual(p.rsplit(util.PATH_SEP, 1)[1], b'theartist')
def test_albumartist_overrides_artist(self):
self.i.artist = u'theartist'
self.i.albumartist = u'something'
self.lib.path_formats = [(u'default', u'$albumartist')]
p = self.i.destination()
self.assertEqual(p.rsplit(util.PATH_SEP, 1)[1], b'something')
def test_unicode_normalized_nfd_on_mac(self):
instr = unicodedata.normalize('NFC', u'caf\xe9')
self.lib.path_formats = [(u'default', instr)]
dest = self.i.destination(platform='darwin', fragment=True)
self.assertEqual(dest, unicodedata.normalize('NFD', instr))
def test_unicode_normalized_nfc_on_linux(self):
instr = unicodedata.normalize('NFD', u'caf\xe9')
self.lib.path_formats = [(u'default', instr)]
dest = self.i.destination(platform='linux', fragment=True)
self.assertEqual(dest, unicodedata.normalize('NFC', instr))
def test_non_mbcs_characters_on_windows(self):
oldfunc = sys.getfilesystemencoding
sys.getfilesystemencoding = lambda: 'mbcs'
try:
self.i.title = u'h\u0259d'
self.lib.path_formats = [(u'default', u'$title')]
p = self.i.destination()
self.assertFalse(b'?' in p)
# We use UTF-8 to encode Windows paths now.
self.assertTrue(u'h\u0259d'.encode('utf-8') in p)
finally:
sys.getfilesystemencoding = oldfunc
def test_unicode_extension_in_fragment(self):
self.lib.path_formats = [(u'default', u'foo')]
self.i.path = util.bytestring_path(u'bar.caf\xe9')
dest = self.i.destination(platform='linux', fragment=True)
self.assertEqual(dest, u'foo.caf\xe9')
def test_asciify_and_replace(self):
config['asciify_paths'] = True
self.lib.replacements = [(re.compile(u'"'), u'q')]
self.lib.directory = b'lib'
self.lib.path_formats = [(u'default', u'$title')]
self.i.title = u'\u201c\u00f6\u2014\u00cf\u201d'
self.assertEqual(self.i.destination(), np('lib/qo--Iq'))
def test_asciify_character_expanding_to_slash(self):
config['asciify_paths'] = True
self.lib.directory = b'lib'
self.lib.path_formats = [(u'default', u'$title')]
self.i.title = u'ab\xa2\xbdd'
self.assertEqual(self.i.destination(), np('lib/abC_1_2d'))
def test_destination_with_replacements(self):
self.lib.directory = b'base'
self.lib.replacements = [(re.compile(r'a'), u'e')]
self.lib.path_formats = [(u'default', u'$album/$title')]
self.i.title = u'foo'
self.i.album = u'bar'
self.assertEqual(self.i.destination(),
np('base/ber/foo'))
@unittest.skip('unimplemented: #359')
def test_destination_with_empty_component(self):
self.lib.directory = b'base'
self.lib.replacements = [(re.compile(r'^$'), u'_')]
self.lib.path_formats = [(u'default', u'$album/$artist/$title')]
self.i.title = u'three'
self.i.artist = u''
self.i.albumartist = u''
self.i.album = u'one'
self.assertEqual(self.i.destination(),
np('base/one/_/three'))
@unittest.skip('unimplemented: #359')
def test_destination_with_empty_final_component(self):
self.lib.directory = b'base'
self.lib.replacements = [(re.compile(r'^$'), u'_')]
self.lib.path_formats = [(u'default', u'$album/$title')]
self.i.title = u''
self.i.album = u'one'
self.i.path = 'foo.mp3'
self.assertEqual(self.i.destination(),
np('base/one/_.mp3'))
def test_legalize_path_one_for_one_replacement(self):
# Use a replacement that should always replace the last X in any
# path component with a Z.
self.lib.replacements = [
(re.compile(r'X$'), u'Z'),
]
# Construct an item whose untruncated path ends with a Y but whose
# truncated version ends with an X.
self.i.title = u'X' * 300 + u'Y'
# The final path should reflect the replacement.
dest = self.i.destination()
self.assertEqual(dest[-2:], b'XZ')
def test_legalize_path_one_for_many_replacement(self):
# Use a replacement that should always replace the last X in any
# path component with four Zs.
self.lib.replacements = [
(re.compile(r'X$'), u'ZZZZ'),
]
# Construct an item whose untruncated path ends with a Y but whose
# truncated version ends with an X.
self.i.title = u'X' * 300 + u'Y'
# The final path should ignore the user replacement and create a path
# of the correct length, containing Xs.
dest = self.i.destination()
self.assertEqual(dest[-2:], b'XX')
class ItemFormattedMappingTest(_common.LibTestCase):
def test_formatted_item_value(self):
formatted = self.i.formatted()
self.assertEqual(formatted['artist'], u'the artist')
def test_get_unset_field(self):
formatted = self.i.formatted()
with self.assertRaises(KeyError):
formatted['other_field']
def test_get_method_with_default(self):
formatted = self.i.formatted()
self.assertEqual(formatted.get('other_field'), u'')
def test_get_method_with_specified_default(self):
formatted = self.i.formatted()
self.assertEqual(formatted.get('other_field', u'default'), u'default')
def test_item_precedence(self):
album = self.lib.add_album([self.i])
album['artist'] = u'foo'
album.store()
self.assertNotEqual(u'foo', self.i.formatted().get('artist'))
def test_album_flex_field(self):
album = self.lib.add_album([self.i])
album['flex'] = u'foo'
album.store()
self.assertEqual(u'foo', self.i.formatted().get('flex'))
def test_album_field_overrides_item_field_for_path(self):
# Make the album inconsistent with the item.
album = self.lib.add_album([self.i])
album.album = u'foo'
album.store()
self.i.album = u'bar'
self.i.store()
# Ensure the album takes precedence.
formatted = self.i.formatted(for_path=True)
self.assertEqual(formatted['album'], u'foo')
def test_artist_falls_back_to_albumartist(self):
self.i.artist = u''
formatted = self.i.formatted()
self.assertEqual(formatted['artist'], u'the album artist')
def test_albumartist_falls_back_to_artist(self):
self.i.albumartist = u''
formatted = self.i.formatted()
self.assertEqual(formatted['albumartist'], u'the artist')
def test_both_artist_and_albumartist_empty(self):
self.i.artist = u''
self.i.albumartist = u''
formatted = self.i.formatted()
self.assertEqual(formatted['albumartist'], u'')
class PathFormattingMixin(object):
"""Utilities for testing path formatting."""
def _setf(self, fmt):
self.lib.path_formats.insert(0, (u'default', fmt))
def _assert_dest(self, dest, i=None):
if i is None:
i = self.i
with _common.platform_posix():
actual = i.destination()
self.assertEqual(actual, dest)
class DestinationFunctionTest(_common.TestCase, PathFormattingMixin):
def setUp(self):
super(DestinationFunctionTest, self).setUp()
self.lib = beets.library.Library(':memory:')
self.lib.directory = b'/base'
self.lib.path_formats = [(u'default', u'path')]
self.i = item(self.lib)
def tearDown(self):
super(DestinationFunctionTest, self).tearDown()
self.lib._connection().close()
def test_upper_case_literal(self):
self._setf(u'%upper{foo}')
self._assert_dest(b'/base/FOO')
def test_upper_case_variable(self):
self._setf(u'%upper{$title}')
self._assert_dest(b'/base/THE TITLE')
def test_title_case_variable(self):
self._setf(u'%title{$title}')
self._assert_dest(b'/base/The Title')
def test_asciify_variable(self):
self._setf(u'%asciify{ab\xa2\xbdd}')
self._assert_dest(b'/base/abC_1_2d')
def test_left_variable(self):
self._setf(u'%left{$title, 3}')
self._assert_dest(b'/base/the')
def test_right_variable(self):
self._setf(u'%right{$title,3}')
self._assert_dest(b'/base/tle')
def test_if_false(self):
self._setf(u'x%if{,foo}')
self._assert_dest(b'/base/x')
def test_if_false_value(self):
self._setf(u'x%if{false,foo}')
self._assert_dest(b'/base/x')
def test_if_true(self):
self._setf(u'%if{bar,foo}')
self._assert_dest(b'/base/foo')
def test_if_else_false(self):
self._setf(u'%if{,foo,baz}')
self._assert_dest(b'/base/baz')
def test_if_else_false_value(self):
self._setf(u'%if{false,foo,baz}')
self._assert_dest(b'/base/baz')
def test_if_int_value(self):
self._setf(u'%if{0,foo,baz}')
self._assert_dest(b'/base/baz')
def test_nonexistent_function(self):
self._setf(u'%foo{bar}')
self._assert_dest(b'/base/%foo{bar}')
def test_if_def_field_return_self(self):
self.i.bar = 3
self._setf(u'%ifdef{bar}')
self._assert_dest(b'/base/3')
def test_if_def_field_not_defined(self):
self._setf(u' %ifdef{bar}/$artist')
self._assert_dest(b'/base/the artist')
def test_if_def_field_not_defined_2(self):
self._setf(u'$artist/%ifdef{bar}')
self._assert_dest(b'/base/the artist')
def test_if_def_true(self):
self._setf(u'%ifdef{artist,cool}')
self._assert_dest(b'/base/cool')
def test_if_def_true_complete(self):
self.i.series = "Now"
self._setf(u'%ifdef{series,$series Series,Albums}/$album')
self._assert_dest(b'/base/Now Series/the album')
def test_if_def_false_complete(self):
self._setf(u'%ifdef{plays,$plays,not_played}')
self._assert_dest(b'/base/not_played')
def test_first(self):
self.i.genres = "Pop; Rock; Classical Crossover"
self._setf(u'%first{$genres}')
self._assert_dest(b'/base/Pop')
def test_first_skip(self):
self.i.genres = "Pop; Rock; Classical Crossover"
self._setf(u'%first{$genres,1,2}')
self._assert_dest(b'/base/Classical Crossover')
def test_first_different_sep(self):
self._setf(u'%first{Alice / Bob / Eve,2,0, / , & }')
self._assert_dest(b'/base/Alice & Bob')
class DisambiguationTest(_common.TestCase, PathFormattingMixin):
def setUp(self):
super(DisambiguationTest, self).setUp()
self.lib = beets.library.Library(':memory:')
self.lib.directory = b'/base'
self.lib.path_formats = [(u'default', u'path')]
self.i1 = item()
self.i1.year = 2001
self.lib.add_album([self.i1])
self.i2 = item()
self.i2.year = 2002
self.lib.add_album([self.i2])
self.lib._connection().commit()
self._setf(u'foo%aunique{albumartist album,year}/$title')
def tearDown(self):
super(DisambiguationTest, self).tearDown()
self.lib._connection().close()
def test_unique_expands_to_disambiguating_year(self):
self._assert_dest(b'/base/foo [2001]/the title', self.i1)
def test_unique_with_default_arguments_uses_albumtype(self):
album2 = self.lib.get_album(self.i1)
album2.albumtype = u'bar'
album2.store()
self._setf(u'foo%aunique{}/$title')
self._assert_dest(b'/base/foo [bar]/the title', self.i1)
def test_unique_expands_to_nothing_for_distinct_albums(self):
album2 = self.lib.get_album(self.i2)
album2.album = u'different album'
album2.store()
self._assert_dest(b'/base/foo/the title', self.i1)
def test_use_fallback_numbers_when_identical(self):
album2 = self.lib.get_album(self.i2)
album2.year = 2001
album2.store()
self._assert_dest(b'/base/foo [1]/the title', self.i1)
self._assert_dest(b'/base/foo [2]/the title', self.i2)
def test_unique_falls_back_to_second_distinguishing_field(self):
self._setf(u'foo%aunique{albumartist album,month year}/$title')
self._assert_dest(b'/base/foo [2001]/the title', self.i1)
def test_unique_sanitized(self):
album2 = self.lib.get_album(self.i2)
album2.year = 2001
album1 = self.lib.get_album(self.i1)
album1.albumtype = u'foo/bar'
album2.store()
album1.store()
self._setf(u'foo%aunique{albumartist album,albumtype}/$title')
self._assert_dest(b'/base/foo [foo_bar]/the title', self.i1)
def test_drop_empty_disambig_string(self):
album1 = self.lib.get_album(self.i1)
album1.albumdisambig = None
album2 = self.lib.get_album(self.i2)
album2.albumdisambig = u'foo'
album1.store()
album2.store()
self._setf(u'foo%aunique{albumartist album,albumdisambig}/$title')
self._assert_dest(b'/base/foo/the title', self.i1)
def test_change_brackets(self):
self._setf(u'foo%aunique{albumartist album,year,()}/$title')
self._assert_dest(b'/base/foo (2001)/the title', self.i1)
def test_remove_brackets(self):
self._setf(u'foo%aunique{albumartist album,year,}/$title')
self._assert_dest(b'/base/foo 2001/the title', self.i1)
class PluginDestinationTest(_common.TestCase):
def setUp(self):
super(PluginDestinationTest, self).setUp()
# Mock beets.plugins.item_field_getters.
self._tv_map = {}
def field_getters():
getters = {}
for key, value in self._tv_map.items():
getters[key] = lambda _: value
return getters
self.old_field_getters = plugins.item_field_getters
plugins.item_field_getters = field_getters
self.lib = beets.library.Library(':memory:')
self.lib.directory = b'/base'
self.lib.path_formats = [(u'default', u'$artist $foo')]
self.i = item(self.lib)
def tearDown(self):
super(PluginDestinationTest, self).tearDown()
plugins.item_field_getters = self.old_field_getters
def _assert_dest(self, dest):
with _common.platform_posix():
the_dest = self.i.destination()
self.assertEqual(the_dest, b'/base/' + dest)
def test_undefined_value_not_substituted(self):
self._assert_dest(b'the artist $foo')
def test_plugin_value_not_substituted(self):
self._tv_map = {
'foo': 'bar',
}
self._assert_dest(b'the artist bar')
def test_plugin_value_overrides_attribute(self):
self._tv_map = {
'artist': 'bar',
}
self._assert_dest(b'bar $foo')
def test_plugin_value_sanitized(self):
self._tv_map = {
'foo': 'bar/baz',
}
self._assert_dest(b'the artist bar_baz')
class AlbumInfoTest(_common.TestCase):
def setUp(self):
super(AlbumInfoTest, self).setUp()
self.lib = beets.library.Library(':memory:')
self.i = item()
self.lib.add_album((self.i,))
def test_albuminfo_reflects_metadata(self):
ai = self.lib.get_album(self.i)
self.assertEqual(ai.mb_albumartistid, self.i.mb_albumartistid)
self.assertEqual(ai.albumartist, self.i.albumartist)
self.assertEqual(ai.album, self.i.album)
self.assertEqual(ai.year, self.i.year)
def test_albuminfo_stores_art(self):
ai = self.lib.get_album(self.i)
ai.artpath = '/my/great/art'
ai.store()
new_ai = self.lib.get_album(self.i)
self.assertEqual(new_ai.artpath, b'/my/great/art')
def test_albuminfo_for_two_items_doesnt_duplicate_row(self):
i2 = item(self.lib)
self.lib.get_album(self.i)
self.lib.get_album(i2)
c = self.lib._connection().cursor()
c.execute('select * from albums where album=?', (self.i.album,))
# Cursor should only return one row.
self.assertNotEqual(c.fetchone(), None)
self.assertEqual(c.fetchone(), None)
def test_individual_tracks_have_no_albuminfo(self):
i2 = item()
i2.album = u'aTotallyDifferentAlbum'
self.lib.add(i2)
ai = self.lib.get_album(i2)
self.assertEqual(ai, None)
def test_get_album_by_id(self):
ai = self.lib.get_album(self.i)
ai = self.lib.get_album(self.i.id)
self.assertNotEqual(ai, None)
def test_album_items_consistent(self):
ai = self.lib.get_album(self.i)
for i in ai.items():
if i.id == self.i.id:
break
else:
self.fail(u"item not found")
def test_albuminfo_changes_affect_items(self):
ai = self.lib.get_album(self.i)
ai.album = u'myNewAlbum'
ai.store()
i = self.lib.items()[0]
self.assertEqual(i.album, u'myNewAlbum')
def test_albuminfo_change_albumartist_changes_items(self):
ai = self.lib.get_album(self.i)
ai.albumartist = u'myNewArtist'
ai.store()
i = self.lib.items()[0]
self.assertEqual(i.albumartist, u'myNewArtist')
self.assertNotEqual(i.artist, u'myNewArtist')
def test_albuminfo_change_artist_does_not_change_items(self):
ai = self.lib.get_album(self.i)
ai.artist = u'myNewArtist'
ai.store()
i = self.lib.items()[0]
self.assertNotEqual(i.artist, u'myNewArtist')
def test_albuminfo_remove_removes_items(self):
item_id = self.i.id
self.lib.get_album(self.i).remove()
c = self.lib._connection().execute(
'SELECT id FROM items WHERE id=?', (item_id,)
)
self.assertEqual(c.fetchone(), None)
def test_removing_last_item_removes_album(self):
self.assertEqual(len(self.lib.albums()), 1)
self.i.remove()
self.assertEqual(len(self.lib.albums()), 0)
def test_noop_albuminfo_changes_affect_items(self):
i = self.lib.items()[0]
i.album = u'foobar'
i.store()
ai = self.lib.get_album(self.i)
ai.album = ai.album
ai.store()
i = self.lib.items()[0]
self.assertEqual(i.album, ai.album)
class ArtDestinationTest(_common.TestCase):
def setUp(self):
super(ArtDestinationTest, self).setUp()
config['art_filename'] = u'artimage'
config['replace'] = {u'X': u'Y'}
self.lib = beets.library.Library(
':memory:', replacements=[(re.compile(u'X'), u'Y')]
)
self.i = item(self.lib)
self.i.path = self.i.destination()
self.ai = self.lib.add_album((self.i,))
def test_art_filename_respects_setting(self):
art = self.ai.art_destination('something.jpg')
new_art = bytestring_path('%sartimage.jpg' % os.path.sep)
self.assertTrue(new_art in art)
def test_art_path_in_item_dir(self):
art = self.ai.art_destination('something.jpg')
track = self.i.destination()
self.assertEqual(os.path.dirname(art), os.path.dirname(track))
def test_art_path_sanitized(self):
config['art_filename'] = u'artXimage'
art = self.ai.art_destination('something.jpg')
self.assertTrue(b'artYimage' in art)
class PathStringTest(_common.TestCase):
def setUp(self):
super(PathStringTest, self).setUp()
self.lib = beets.library.Library(':memory:')
self.i = item(self.lib)
def test_item_path_is_bytestring(self):
self.assertTrue(isinstance(self.i.path, bytes))
def test_fetched_item_path_is_bytestring(self):
i = list(self.lib.items())[0]
self.assertTrue(isinstance(i.path, bytes))
def test_unicode_path_becomes_bytestring(self):
self.i.path = u'unicodepath'
self.assertTrue(isinstance(self.i.path, bytes))
def test_unicode_in_database_becomes_bytestring(self):
self.lib._connection().execute("""
update items set path=? where id=?
""", (self.i.id, u'somepath'))
i = list(self.lib.items())[0]
self.assertTrue(isinstance(i.path, bytes))
def test_special_chars_preserved_in_database(self):
path = u'b\xe1r'.encode('utf-8')
self.i.path = path
self.i.store()
i = list(self.lib.items())[0]
self.assertEqual(i.path, path)
def test_special_char_path_added_to_database(self):
self.i.remove()
path = u'b\xe1r'.encode('utf-8')
i = item()
i.path = path
self.lib.add(i)
i = list(self.lib.items())[0]
self.assertEqual(i.path, path)
def test_destination_returns_bytestring(self):
self.i.artist = u'b\xe1r'
dest = self.i.destination()
self.assertTrue(isinstance(dest, bytes))
def test_art_destination_returns_bytestring(self):
self.i.artist = u'b\xe1r'
alb = self.lib.add_album([self.i])
dest = alb.art_destination(u'image.jpg')
self.assertTrue(isinstance(dest, bytes))
def test_artpath_stores_special_chars(self):
path = b'b\xe1r'
alb = self.lib.add_album([self.i])
alb.artpath = path
alb.store()
alb = self.lib.get_album(self.i)
self.assertEqual(path, alb.artpath)
def test_sanitize_path_with_special_chars(self):
path = u'b\xe1r?'
new_path = util.sanitize_path(path)
self.assertTrue(new_path.startswith(u'b\xe1r'))
def test_sanitize_path_returns_unicode(self):
path = u'b\xe1r?'
new_path = util.sanitize_path(path)
self.assertTrue(isinstance(new_path, six.text_type))
def test_unicode_artpath_becomes_bytestring(self):
alb = self.lib.add_album([self.i])
alb.artpath = u'somep\xe1th'
self.assertTrue(isinstance(alb.artpath, bytes))
def test_unicode_artpath_in_database_decoded(self):
alb = self.lib.add_album([self.i])
self.lib._connection().execute(
"update albums set artpath=? where id=?",
(u'somep\xe1th', alb.id)
)
alb = self.lib.get_album(alb.id)
self.assertTrue(isinstance(alb.artpath, bytes))
class MtimeTest(_common.TestCase):
def setUp(self):
super(MtimeTest, self).setUp()
self.ipath = os.path.join(self.temp_dir, b'testfile.mp3')
shutil.copy(os.path.join(_common.RSRC, b'full.mp3'), self.ipath)
self.i = beets.library.Item.from_path(self.ipath)
self.lib = beets.library.Library(':memory:')
self.lib.add(self.i)
def tearDown(self):
super(MtimeTest, self).tearDown()
if os.path.exists(self.ipath):
os.remove(self.ipath)
def _mtime(self):
return int(os.path.getmtime(self.ipath))
def test_mtime_initially_up_to_date(self):
self.assertGreaterEqual(self.i.mtime, self._mtime())
def test_mtime_reset_on_db_modify(self):
self.i.title = u'something else'
self.assertLess(self.i.mtime, self._mtime())
def test_mtime_up_to_date_after_write(self):
self.i.title = u'something else'
self.i.write()
self.assertGreaterEqual(self.i.mtime, self._mtime())
def test_mtime_up_to_date_after_read(self):
self.i.title = u'something else'
self.i.read()
self.assertGreaterEqual(self.i.mtime, self._mtime())
class ImportTimeTest(_common.TestCase):
def setUp(self):
super(ImportTimeTest, self).setUp()
self.lib = beets.library.Library(':memory:')
def added(self):
self.track = item()
self.album = self.lib.add_album((self.track,))
self.assertGreater(self.album.added, 0)
self.assertGreater(self.track.added, 0)
def test_atime_for_singleton(self):
self.singleton = item(self.lib)
self.assertGreater(self.singleton.added, 0)
class TemplateTest(_common.LibTestCase):
def test_year_formatted_in_template(self):
self.i.year = 123
self.i.store()
self.assertEqual(self.i.evaluate_template('$year'), u'0123')
def test_album_flexattr_appears_in_item_template(self):
self.album = self.lib.add_album([self.i])
self.album.foo = u'baz'
self.album.store()
self.assertEqual(self.i.evaluate_template('$foo'), u'baz')
def test_album_and_item_format(self):
config['format_album'] = u'foö $foo'
album = beets.library.Album()
album.foo = u'bar'
album.tagada = u'togodo'
self.assertEqual(u"{0}".format(album), u"foö bar")
self.assertEqual(u"{0:$tagada}".format(album), u"togodo")
self.assertEqual(six.text_type(album), u"foö bar")
self.assertEqual(bytes(album), b"fo\xc3\xb6 bar")
config['format_item'] = u'bar $foo'
item = beets.library.Item()
item.foo = u'bar'
item.tagada = u'togodo'
self.assertEqual(u"{0}".format(item), u"bar bar")
self.assertEqual(u"{0:$tagada}".format(item), u"togodo")
class UnicodePathTest(_common.LibTestCase):
def test_unicode_path(self):
self.i.path = os.path.join(_common.RSRC,
u'unicode\u2019d.mp3'.encode('utf-8'))
# If there are any problems with unicode paths, we will raise
# here and fail.
self.i.read()
self.i.write()
class WriteTest(unittest.TestCase, TestHelper):
def setUp(self):
self.setup_beets()
def tearDown(self):
self.teardown_beets()
def test_write_nonexistant(self):
item = self.create_item()
item.path = b'/path/does/not/exist'
with self.assertRaises(beets.library.ReadError):
item.write()
def test_no_write_permission(self):
item = self.add_item_fixture()
path = syspath(item.path)
os.chmod(path, stat.S_IRUSR)
try:
self.assertRaises(beets.library.WriteError, item.write)
finally:
# Restore write permissions so the file can be cleaned up.
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
def test_write_with_custom_path(self):
item = self.add_item_fixture()
custom_path = os.path.join(self.temp_dir, b'custom.mp3')
shutil.copy(syspath(item.path), syspath(custom_path))
item['artist'] = 'new artist'
self.assertNotEqual(MediaFile(syspath(custom_path)).artist,
'new artist')
self.assertNotEqual(MediaFile(syspath(item.path)).artist,
'new artist')
item.write(custom_path)
self.assertEqual(MediaFile(syspath(custom_path)).artist, 'new artist')
self.assertNotEqual(MediaFile(syspath(item.path)).artist, 'new artist')
def test_write_custom_tags(self):
item = self.add_item_fixture(artist='old artist')
item.write(tags={'artist': 'new artist'})
self.assertNotEqual(item.artist, 'new artist')
self.assertEqual(MediaFile(syspath(item.path)).artist, 'new artist')
def test_write_date_field(self):
# Since `date` is not a MediaField, this should do nothing.
item = self.add_item_fixture()
clean_year = item.year
item.date = u'foo'
item.write()
self.assertEqual(MediaFile(syspath(item.path)).year, clean_year)
class ItemReadTest(unittest.TestCase):
def test_unreadable_raise_read_error(self):
unreadable = os.path.join(_common.RSRC, b'image-2x3.png')
item = beets.library.Item()
with self.assertRaises(beets.library.ReadError) as cm:
item.read(unreadable)
self.assertIsInstance(cm.exception.reason,
beets.mediafile.UnreadableFileError)
def test_nonexistent_raise_read_error(self):
item = beets.library.Item()
with self.assertRaises(beets.library.ReadError):
item.read('/thisfiledoesnotexist')
class FilesizeTest(unittest.TestCase, TestHelper):
def setUp(self):
self.setup_beets()
def tearDown(self):
self.teardown_beets()
def test_filesize(self):
item = self.add_item_fixture()
self.assertNotEqual(item.filesize, 0)
def test_nonexistent_file(self):
item = beets.library.Item()
self.assertEqual(item.filesize, 0)
class ParseQueryTest(unittest.TestCase):
def test_parse_invalid_query_string(self):
with self.assertRaises(beets.dbcore.InvalidQueryError) as raised:
beets.library.parse_query_string(u'foo"', None)
self.assertIsInstance(raised.exception,
beets.dbcore.query.ParsingError)
def test_parse_bytes(self):
with self.assertRaises(AssertionError):
beets.library.parse_query_string(b"query", None)
class LibraryFieldTypesTest(unittest.TestCase):
"""Test format() and parse() for library-specific field types"""
def test_datetype(self):
t = beets.library.DateType()
# format
time_format = beets.config['time_format'].as_str()
time_local = time.strftime(time_format,
time.localtime(123456789))
self.assertEqual(time_local, t.format(123456789))
# parse
self.assertEqual(123456789.0, t.parse(time_local))
self.assertEqual(123456789.0, t.parse(u'123456789.0'))
self.assertEqual(t.null, t.parse(u'not123456789.0'))
self.assertEqual(t.null, t.parse(u'1973-11-29'))
def test_pathtype(self):
t = beets.library.PathType()
# format
self.assertEqual('/tmp', t.format('/tmp'))
self.assertEqual(u'/tmp/\xe4lbum', t.format(u'/tmp/\u00e4lbum'))
# parse
self.assertEqual(np(b'/tmp'), t.parse('/tmp'))
self.assertEqual(np(b'/tmp/\xc3\xa4lbum'),
t.parse(u'/tmp/\u00e4lbum/'))
def test_musicalkey(self):
t = beets.library.MusicalKey()
# parse
self.assertEqual(u'C#m', t.parse(u'c#m'))
self.assertEqual(u'Gm', t.parse(u'g minor'))
self.assertEqual(u'Not c#m', t.parse(u'not C#m'))
def test_durationtype(self):
t = beets.library.DurationType()
# format
self.assertEqual(u'1:01', t.format(61.23))
self.assertEqual(u'60:01', t.format(3601.23))
self.assertEqual(u'0:00', t.format(None))
# parse
self.assertEqual(61.0, t.parse(u'1:01'))
self.assertEqual(61.23, t.parse(u'61.23'))
self.assertEqual(3601.0, t.parse(u'60:01'))
self.assertEqual(t.null, t.parse(u'1:00:01'))
self.assertEqual(t.null, t.parse(u'not61.23'))
# config format_raw_length
beets.config['format_raw_length'] = True
self.assertEqual(61.23, t.format(61.23))
self.assertEqual(3601.23, t.format(3601.23))
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/test_fetchart.py 0000644 0000765 0000024 00000003721 13025125203 017754 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Thomas Scholtes.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
from __future__ import division, absolute_import, print_function
import os
import unittest
from test.helper import TestHelper
from beets import util
class FetchartCliTest(unittest.TestCase, TestHelper):
def setUp(self):
self.setup_beets()
self.load_plugins('fetchart')
self.config['fetchart']['cover_names'] = 'c\xc3\xb6ver.jpg'
self.config['art_filename'] = 'mycover'
self.album = self.add_album()
def tearDown(self):
self.unload_plugins()
self.teardown_beets()
def test_set_art_from_folder(self):
self.touch(b'c\xc3\xb6ver.jpg', dir=self.album.path, content='IMAGE')
self.run_command('fetchart')
cover_path = os.path.join(self.album.path, b'mycover.jpg')
self.album.load()
self.assertEqual(self.album['artpath'], cover_path)
with open(util.syspath(cover_path), 'r') as f:
self.assertEqual(f.read(), 'IMAGE')
def test_filesystem_does_not_pick_up_folder(self):
os.makedirs(os.path.join(self.album.path, b'mycover.jpg'))
self.run_command('fetchart')
self.album.load()
self.assertEqual(self.album['artpath'], None)
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/test_player.py 0000644 0000765 0000024 00000004337 13025125203 017454 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Tests for BPD and music playing.
"""
from __future__ import division, absolute_import, print_function
import unittest
from beetsplug import bpd
class CommandParseTest(unittest.TestCase):
def test_no_args(self):
s = r'command'
c = bpd.Command(s)
self.assertEqual(c.name, u'command')
self.assertEqual(c.args, [])
def test_one_unquoted_arg(self):
s = r'command hello'
c = bpd.Command(s)
self.assertEqual(c.name, u'command')
self.assertEqual(c.args, [u'hello'])
def test_two_unquoted_args(self):
s = r'command hello there'
c = bpd.Command(s)
self.assertEqual(c.name, u'command')
self.assertEqual(c.args, [u'hello', u'there'])
def test_one_quoted_arg(self):
s = r'command "hello there"'
c = bpd.Command(s)
self.assertEqual(c.name, u'command')
self.assertEqual(c.args, [u'hello there'])
def test_heterogenous_args(self):
s = r'command "hello there" sir'
c = bpd.Command(s)
self.assertEqual(c.name, u'command')
self.assertEqual(c.args, [u'hello there', u'sir'])
def test_quote_in_arg(self):
s = r'command "hello \" there"'
c = bpd.Command(s)
self.assertEqual(c.args, [u'hello " there'])
def test_backslash_in_arg(self):
s = r'command "hello \\ there"'
c = bpd.Command(s)
self.assertEqual(c.args, [u'hello \ there'])
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/test_mpdstats.py 0000644 0000765 0000024 00000005707 13025125203 020021 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
from __future__ import division, absolute_import, print_function
import unittest
from mock import Mock, patch, call, ANY
from test.helper import TestHelper
from beets.library import Item
from beetsplug.mpdstats import MPDStats
from beets import util
class MPDStatsTest(unittest.TestCase, TestHelper):
def setUp(self):
self.setup_beets()
self.load_plugins('mpdstats')
def tearDown(self):
self.teardown_beets()
self.unload_plugins()
def test_update_rating(self):
item = Item(title=u'title', path='', id=1)
item.add(self.lib)
log = Mock()
mpdstats = MPDStats(self.lib, log)
self.assertFalse(mpdstats.update_rating(item, True))
self.assertFalse(mpdstats.update_rating(None, True))
def test_get_item(self):
item_path = util.normpath('/foo/bar.flac')
item = Item(title=u'title', path=item_path, id=1)
item.add(self.lib)
log = Mock()
mpdstats = MPDStats(self.lib, log)
self.assertEqual(str(mpdstats.get_item(item_path)), str(item))
self.assertIsNone(mpdstats.get_item('/some/non-existing/path'))
self.assertIn(u'item not found:', log.info.call_args[0][0])
FAKE_UNKNOWN_STATE = 'some-unknown-one'
STATUSES = [{'state': FAKE_UNKNOWN_STATE},
{'state': u'pause'},
{'state': u'play', 'songid': 1, 'time': u'0:1'},
{'state': u'stop'}]
EVENTS = [["player"]] * (len(STATUSES) - 1) + [KeyboardInterrupt]
item_path = util.normpath('/foo/bar.flac')
@patch("beetsplug.mpdstats.MPDClientWrapper", return_value=Mock(**{
"events.side_effect": EVENTS, "status.side_effect": STATUSES,
"playlist.return_value": {1: item_path}}))
def test_run_mpdstats(self, mpd_mock):
item = Item(title=u'title', path=self.item_path, id=1)
item.add(self.lib)
log = Mock()
try:
MPDStats(self.lib, log).run()
except KeyboardInterrupt:
pass
log.debug.assert_has_calls(
[call(u'unhandled status "{0}"', ANY)])
log.info.assert_has_calls(
[call(u'pause'), call(u'playing {0}', ANY), call(u'stop')])
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/test_logging.py 0000644 0000765 0000024 00000024503 13120341455 017610 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
"""Stupid tests that ensure logging works as expected"""
from __future__ import division, absolute_import, print_function
import sys
import threading
import logging as log
from six import StringIO
import unittest
import beets.logging as blog
from beets import plugins, ui
import beetsplug
from test import _common
from test._common import TestCase
from test import helper
import six
class LoggingTest(TestCase):
def test_logging_management(self):
l1 = log.getLogger("foo123")
l2 = blog.getLogger("foo123")
self.assertEqual(l1, l2)
self.assertEqual(l1.__class__, log.Logger)
l3 = blog.getLogger("bar123")
l4 = log.getLogger("bar123")
self.assertEqual(l3, l4)
self.assertEqual(l3.__class__, blog.BeetsLogger)
self.assertIsInstance(l3, (blog.StrFormatLogger,
blog.ThreadLocalLevelLogger))
l5 = l3.getChild("shalala")
self.assertEqual(l5.__class__, blog.BeetsLogger)
l6 = blog.getLogger()
self.assertNotEqual(l1, l6)
def test_str_format_logging(self):
l = blog.getLogger("baz123")
stream = StringIO()
handler = log.StreamHandler(stream)
l.addHandler(handler)
l.propagate = False
l.warning(u"foo {0} {bar}", "oof", bar=u"baz")
handler.flush()
self.assertTrue(stream.getvalue(), u"foo oof baz")
class LoggingLevelTest(unittest.TestCase, helper.TestHelper):
class DummyModule(object):
class DummyPlugin(plugins.BeetsPlugin):
def __init__(self):
plugins.BeetsPlugin.__init__(self, 'dummy')
self.import_stages = [self.import_stage]
self.register_listener('dummy_event', self.listener)
def log_all(self, name):
self._log.debug(u'debug ' + name)
self._log.info(u'info ' + name)
self._log.warning(u'warning ' + name)
def commands(self):
cmd = ui.Subcommand('dummy')
cmd.func = lambda _, __, ___: self.log_all('cmd')
return (cmd,)
def import_stage(self, session, task):
self.log_all('import_stage')
def listener(self):
self.log_all('listener')
def setUp(self):
sys.modules['beetsplug.dummy'] = self.DummyModule
beetsplug.dummy = self.DummyModule
self.setup_beets()
self.load_plugins('dummy')
def tearDown(self):
self.unload_plugins()
self.teardown_beets()
del beetsplug.dummy
sys.modules.pop('beetsplug.dummy')
self.DummyModule.DummyPlugin.listeners = None
self.DummyModule.DummyPlugin._raw_listeners = None
def test_command_level0(self):
self.config['verbose'] = 0
with helper.capture_log() as logs:
self.run_command('dummy')
self.assertIn(u'dummy: warning cmd', logs)
self.assertIn(u'dummy: info cmd', logs)
self.assertNotIn(u'dummy: debug cmd', logs)
def test_command_level1(self):
self.config['verbose'] = 1
with helper.capture_log() as logs:
self.run_command('dummy')
self.assertIn(u'dummy: warning cmd', logs)
self.assertIn(u'dummy: info cmd', logs)
self.assertIn(u'dummy: debug cmd', logs)
def test_command_level2(self):
self.config['verbose'] = 2
with helper.capture_log() as logs:
self.run_command('dummy')
self.assertIn(u'dummy: warning cmd', logs)
self.assertIn(u'dummy: info cmd', logs)
self.assertIn(u'dummy: debug cmd', logs)
def test_listener_level0(self):
self.config['verbose'] = 0
with helper.capture_log() as logs:
plugins.send('dummy_event')
self.assertIn(u'dummy: warning listener', logs)
self.assertNotIn(u'dummy: info listener', logs)
self.assertNotIn(u'dummy: debug listener', logs)
def test_listener_level1(self):
self.config['verbose'] = 1
with helper.capture_log() as logs:
plugins.send('dummy_event')
self.assertIn(u'dummy: warning listener', logs)
self.assertIn(u'dummy: info listener', logs)
self.assertNotIn(u'dummy: debug listener', logs)
def test_listener_level2(self):
self.config['verbose'] = 2
with helper.capture_log() as logs:
plugins.send('dummy_event')
self.assertIn(u'dummy: warning listener', logs)
self.assertIn(u'dummy: info listener', logs)
self.assertIn(u'dummy: debug listener', logs)
def test_import_stage_level0(self):
self.config['verbose'] = 0
with helper.capture_log() as logs:
importer = self.create_importer()
importer.run()
self.assertIn(u'dummy: warning import_stage', logs)
self.assertNotIn(u'dummy: info import_stage', logs)
self.assertNotIn(u'dummy: debug import_stage', logs)
def test_import_stage_level1(self):
self.config['verbose'] = 1
with helper.capture_log() as logs:
importer = self.create_importer()
importer.run()
self.assertIn(u'dummy: warning import_stage', logs)
self.assertIn(u'dummy: info import_stage', logs)
self.assertNotIn(u'dummy: debug import_stage', logs)
def test_import_stage_level2(self):
self.config['verbose'] = 2
with helper.capture_log() as logs:
importer = self.create_importer()
importer.run()
self.assertIn(u'dummy: warning import_stage', logs)
self.assertIn(u'dummy: info import_stage', logs)
self.assertIn(u'dummy: debug import_stage', logs)
@_common.slow_test()
class ConcurrentEventsTest(TestCase, helper.TestHelper):
"""Similar to LoggingLevelTest but lower-level and focused on multiple
events interaction. Since this is a bit heavy we don't do it in
LoggingLevelTest.
"""
class DummyPlugin(plugins.BeetsPlugin):
def __init__(self, test_case):
plugins.BeetsPlugin.__init__(self, 'dummy')
self.register_listener('dummy_event1', self.listener1)
self.register_listener('dummy_event2', self.listener2)
self.lock1 = threading.Lock()
self.lock2 = threading.Lock()
self.test_case = test_case
self.exc_info = None
self.t1_step = self.t2_step = 0
def log_all(self, name):
self._log.debug(u'debug ' + name)
self._log.info(u'info ' + name)
self._log.warning(u'warning ' + name)
def listener1(self):
try:
self.test_case.assertEqual(self._log.level, log.INFO)
self.t1_step = 1
self.lock1.acquire()
self.test_case.assertEqual(self._log.level, log.INFO)
self.t1_step = 2
except Exception:
import sys
self.exc_info = sys.exc_info()
def listener2(self):
try:
self.test_case.assertEqual(self._log.level, log.DEBUG)
self.t2_step = 1
self.lock2.acquire()
self.test_case.assertEqual(self._log.level, log.DEBUG)
self.t2_step = 2
except Exception:
import sys
self.exc_info = sys.exc_info()
def setUp(self):
self.setup_beets(disk=True)
def tearDown(self):
self.teardown_beets()
def test_concurrent_events(self):
dp = self.DummyPlugin(self)
def check_dp_exc():
if dp.exc_info:
six.reraise(dp.exc_info[1], None, dp.exc_info[2])
try:
dp.lock1.acquire()
dp.lock2.acquire()
self.assertEqual(dp._log.level, log.NOTSET)
self.config['verbose'] = 1
t1 = threading.Thread(target=dp.listeners['dummy_event1'][0])
t1.start() # blocked. t1 tested its log level
while dp.t1_step != 1:
check_dp_exc()
self.assertTrue(t1.is_alive())
self.assertEqual(dp._log.level, log.NOTSET)
self.config['verbose'] = 2
t2 = threading.Thread(target=dp.listeners['dummy_event2'][0])
t2.start() # blocked. t2 tested its log level
while dp.t2_step != 1:
check_dp_exc()
self.assertTrue(t2.is_alive())
self.assertEqual(dp._log.level, log.NOTSET)
dp.lock1.release() # dummy_event1 tests its log level + finishes
while dp.t1_step != 2:
check_dp_exc()
t1.join(.1)
self.assertFalse(t1.is_alive())
self.assertTrue(t2.is_alive())
self.assertEqual(dp._log.level, log.NOTSET)
dp.lock2.release() # dummy_event2 tests its log level + finishes
while dp.t2_step != 2:
check_dp_exc()
t2.join(.1)
self.assertFalse(t2.is_alive())
except Exception:
print(u"Alive threads:", threading.enumerate())
if dp.lock1.locked():
print(u"Releasing lock1 after exception in test")
dp.lock1.release()
if dp.lock2.locked():
print(u"Releasing lock2 after exception in test")
dp.lock2.release()
print(u"Alive threads:", threading.enumerate())
raise
def test_root_logger_levels(self):
"""Root logger level should be shared between threads.
"""
self.config['threaded'] = True
blog.getLogger('beets').set_global_level(blog.WARNING)
with helper.capture_log() as logs:
importer = self.create_importer()
importer.run()
self.assertEqual(logs, [])
blog.getLogger('beets').set_global_level(blog.INFO)
with helper.capture_log() as logs:
importer = self.create_importer()
importer.run()
for l in logs:
self.assertIn(u"import", l)
self.assertIn(u"album", l)
blog.getLogger('beets').set_global_level(blog.DEBUG)
with helper.capture_log() as logs:
importer = self.create_importer()
importer.run()
self.assertIn(u"Sending event: database_change", logs)
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/test_filefilter.py 0000644 0000765 0000024 00000017774 13025125203 020316 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Malte Ried.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Tests for the `filefilter` plugin.
"""
from __future__ import division, absolute_import, print_function
import os
import shutil
import unittest
from test import _common
from test.helper import capture_log
from test.test_importer import ImportHelper
from beets import config
from beets.mediafile import MediaFile
from beets.util import displayable_path, bytestring_path
from beetsplug.filefilter import FileFilterPlugin
class FileFilterPluginTest(unittest.TestCase, ImportHelper):
def setUp(self):
self.setup_beets()
self.__create_import_dir(2)
self._setup_import_session()
config['import']['pretend'] = True
def tearDown(self):
self.teardown_beets()
def __copy_file(self, dest_path, metadata):
# Copy files
resource_path = os.path.join(_common.RSRC, b'full.mp3')
shutil.copy(resource_path, dest_path)
medium = MediaFile(dest_path)
# Set metadata
for attr in metadata:
setattr(medium, attr, metadata[attr])
medium.save()
def __create_import_dir(self, count):
self.import_dir = os.path.join(self.temp_dir, b'testsrcdir')
if os.path.isdir(self.import_dir):
shutil.rmtree(self.import_dir)
self.artist_path = os.path.join(self.import_dir, b'artist')
self.album_path = os.path.join(self.artist_path, b'album')
self.misc_path = os.path.join(self.import_dir, b'misc')
os.makedirs(self.album_path)
os.makedirs(self.misc_path)
metadata = {
'artist': 'Tag Artist',
'album': 'Tag Album',
'albumartist': None,
'mb_trackid': None,
'mb_albumid': None,
'comp': None,
}
self.album_paths = []
for i in range(count):
metadata['track'] = i + 1
metadata['title'] = 'Tag Title Album %d' % (i + 1)
track_file = bytestring_path('%02d - track.mp3' % (i + 1))
dest_path = os.path.join(self.album_path, track_file)
self.__copy_file(dest_path, metadata)
self.album_paths.append(dest_path)
self.artist_paths = []
metadata['album'] = None
for i in range(count):
metadata['track'] = i + 10
metadata['title'] = 'Tag Title Artist %d' % (i + 1)
track_file = bytestring_path('track_%d.mp3' % (i + 1))
dest_path = os.path.join(self.artist_path, track_file)
self.__copy_file(dest_path, metadata)
self.artist_paths.append(dest_path)
self.misc_paths = []
for i in range(count):
metadata['artist'] = 'Artist %d' % (i + 42)
metadata['track'] = i + 5
metadata['title'] = 'Tag Title Misc %d' % (i + 1)
track_file = bytestring_path('track_%d.mp3' % (i + 1))
dest_path = os.path.join(self.misc_path, track_file)
self.__copy_file(dest_path, metadata)
self.misc_paths.append(dest_path)
def __run(self, expected_lines, singletons=False):
self.load_plugins('filefilter')
import_files = [self.import_dir]
self._setup_import_session(singletons=singletons)
self.importer.paths = import_files
with capture_log() as logs:
self.importer.run()
self.unload_plugins()
FileFilterPlugin.listeners = None
logs = [line for line in logs if not line.startswith('Sending event:')]
self.assertEqual(logs, expected_lines)
def test_import_default(self):
""" The default configuration should import everything.
"""
self.__run([
'Album: %s' % displayable_path(self.artist_path),
' %s' % displayable_path(self.artist_paths[0]),
' %s' % displayable_path(self.artist_paths[1]),
'Album: %s' % displayable_path(self.album_path),
' %s' % displayable_path(self.album_paths[0]),
' %s' % displayable_path(self.album_paths[1]),
'Album: %s' % displayable_path(self.misc_path),
' %s' % displayable_path(self.misc_paths[0]),
' %s' % displayable_path(self.misc_paths[1])
])
def test_import_nothing(self):
config['filefilter']['path'] = 'not_there'
self.__run(['No files imported from %s' % displayable_path(
self.import_dir)])
# Global options
def test_import_global(self):
config['filefilter']['path'] = '.*track_1.*\.mp3'
self.__run([
'Album: %s' % displayable_path(self.artist_path),
' %s' % displayable_path(self.artist_paths[0]),
'Album: %s' % displayable_path(self.misc_path),
' %s' % displayable_path(self.misc_paths[0]),
])
self.__run([
'Singleton: %s' % displayable_path(self.artist_paths[0]),
'Singleton: %s' % displayable_path(self.misc_paths[0])
], singletons=True)
# Album options
def test_import_album(self):
config['filefilter']['album_path'] = '.*track_1.*\.mp3'
self.__run([
'Album: %s' % displayable_path(self.artist_path),
' %s' % displayable_path(self.artist_paths[0]),
'Album: %s' % displayable_path(self.misc_path),
' %s' % displayable_path(self.misc_paths[0]),
])
self.__run([
'Singleton: %s' % displayable_path(self.artist_paths[0]),
'Singleton: %s' % displayable_path(self.artist_paths[1]),
'Singleton: %s' % displayable_path(self.album_paths[0]),
'Singleton: %s' % displayable_path(self.album_paths[1]),
'Singleton: %s' % displayable_path(self.misc_paths[0]),
'Singleton: %s' % displayable_path(self.misc_paths[1])
], singletons=True)
# Singleton options
def test_import_singleton(self):
config['filefilter']['singleton_path'] = '.*track_1.*\.mp3'
self.__run([
'Singleton: %s' % displayable_path(self.artist_paths[0]),
'Singleton: %s' % displayable_path(self.misc_paths[0])
], singletons=True)
self.__run([
'Album: %s' % displayable_path(self.artist_path),
' %s' % displayable_path(self.artist_paths[0]),
' %s' % displayable_path(self.artist_paths[1]),
'Album: %s' % displayable_path(self.album_path),
' %s' % displayable_path(self.album_paths[0]),
' %s' % displayable_path(self.album_paths[1]),
'Album: %s' % displayable_path(self.misc_path),
' %s' % displayable_path(self.misc_paths[0]),
' %s' % displayable_path(self.misc_paths[1])
])
# Album and singleton options
def test_import_both(self):
config['filefilter']['album_path'] = '.*track_1.*\.mp3'
config['filefilter']['singleton_path'] = '.*track_2.*\.mp3'
self.__run([
'Album: %s' % displayable_path(self.artist_path),
' %s' % displayable_path(self.artist_paths[0]),
'Album: %s' % displayable_path(self.misc_path),
' %s' % displayable_path(self.misc_paths[0]),
])
self.__run([
'Singleton: %s' % displayable_path(self.artist_paths[1]),
'Singleton: %s' % displayable_path(self.misc_paths[1])
], singletons=True)
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/test_embyupdate.py 0000644 0000765 0000024 00000023645 13025125203 020322 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
from __future__ import division, absolute_import, print_function
from test.helper import TestHelper
from beetsplug import embyupdate
import unittest
import responses
class EmbyUpdateTest(unittest.TestCase, TestHelper):
def setUp(self):
self.setup_beets()
self.load_plugins('embyupdate')
self.config['emby'] = {
u'host': u'localhost',
u'port': 8096,
u'username': u'username',
u'password': u'password'
}
def tearDown(self):
self.teardown_beets()
self.unload_plugins()
def test_api_url_only_name(self):
self.assertEqual(
embyupdate.api_url(self.config['emby']['host'].get(),
self.config['emby']['port'].get(),
'/Library/Refresh'),
'http://localhost:8096/Library/Refresh?format=json'
)
def test_api_url_http(self):
self.assertEqual(
embyupdate.api_url(u'http://localhost',
self.config['emby']['port'].get(),
'/Library/Refresh'),
'http://localhost:8096/Library/Refresh?format=json'
)
def test_api_url_https(self):
self.assertEqual(
embyupdate.api_url(u'https://localhost',
self.config['emby']['port'].get(),
'/Library/Refresh'),
'https://localhost:8096/Library/Refresh?format=json'
)
def test_password_data(self):
self.assertEqual(
embyupdate.password_data(self.config['emby']['username'].get(),
self.config['emby']['password'].get()),
{
'username': 'username',
'password': '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8',
'passwordMd5': '5f4dcc3b5aa765d61d8327deb882cf99'
}
)
def test_create_header_no_token(self):
self.assertEqual(
embyupdate.create_headers('e8837bc1-ad67-520e-8cd2-f629e3155721'),
{
'x-emby-authorization': (
'MediaBrowser '
'UserId="e8837bc1-ad67-520e-8cd2-f629e3155721", '
'Client="other", '
'Device="beets", '
'DeviceId="beets", '
'Version="0.0.0"'
)
}
)
def test_create_header_with_token(self):
self.assertEqual(
embyupdate.create_headers('e8837bc1-ad67-520e-8cd2-f629e3155721',
token='abc123'),
{
'x-emby-authorization': (
'MediaBrowser '
'UserId="e8837bc1-ad67-520e-8cd2-f629e3155721", '
'Client="other", '
'Device="beets", '
'DeviceId="beets", '
'Version="0.0.0"'
),
'x-mediabrowser-token': 'abc123'
}
)
@responses.activate
def test_get_token(self):
body = ('{"User":{"Name":"username", '
'"ServerId":"1efa5077976bfa92bc71652404f646ec",'
'"Id":"2ec276a2642e54a19b612b9418a8bd3b","HasPassword":true,'
'"HasConfiguredPassword":true,'
'"HasConfiguredEasyPassword":false,'
'"LastLoginDate":"2015-11-09T08:35:03.6357440Z",'
'"LastActivityDate":"2015-11-09T08:35:03.6665060Z",'
'"Configuration":{"AudioLanguagePreference":"",'
'"PlayDefaultAudioTrack":true,"SubtitleLanguagePreference":"",'
'"DisplayMissingEpisodes":false,'
'"DisplayUnairedEpisodes":false,'
'"GroupMoviesIntoBoxSets":false,'
'"DisplayChannelsWithinViews":[],'
'"ExcludeFoldersFromGrouping":[],"GroupedFolders":[],'
'"SubtitleMode":"Default","DisplayCollectionsView":true,'
'"DisplayFoldersView":false,"EnableLocalPassword":false,'
'"OrderedViews":[],"IncludeTrailersInSuggestions":true,'
'"EnableCinemaMode":true,"LatestItemsExcludes":[],'
'"PlainFolderViews":[],"HidePlayedInLatest":true,'
'"DisplayChannelsInline":false},'
'"Policy":{"IsAdministrator":true,"IsHidden":false,'
'"IsDisabled":false,"BlockedTags":[],'
'"EnableUserPreferenceAccess":true,"AccessSchedules":[],'
'"BlockUnratedItems":[],'
'"EnableRemoteControlOfOtherUsers":false,'
'"EnableSharedDeviceControl":true,'
'"EnableLiveTvManagement":true,"EnableLiveTvAccess":true,'
'"EnableMediaPlayback":true,'
'"EnableAudioPlaybackTranscoding":true,'
'"EnableVideoPlaybackTranscoding":true,'
'"EnableContentDeletion":false,'
'"EnableContentDownloading":true,"EnableSync":true,'
'"EnableSyncTranscoding":true,"EnabledDevices":[],'
'"EnableAllDevices":true,"EnabledChannels":[],'
'"EnableAllChannels":true,"EnabledFolders":[],'
'"EnableAllFolders":true,"InvalidLoginAttemptCount":0,'
'"EnablePublicSharing":true}},'
'"SessionInfo":{"SupportedCommands":[],'
'"QueueableMediaTypes":[],"PlayableMediaTypes":[],'
'"Id":"89f3b33f8b3a56af22088733ad1d76b3",'
'"UserId":"2ec276a2642e54a19b612b9418a8bd3b",'
'"UserName":"username","AdditionalUsers":[],'
'"ApplicationVersion":"Unknown version",'
'"Client":"Unknown app",'
'"LastActivityDate":"2015-11-09T08:35:03.6665060Z",'
'"DeviceName":"Unknown device","DeviceId":"Unknown device id",'
'"SupportsRemoteControl":false,"PlayState":{"CanSeek":false,'
'"IsPaused":false,"IsMuted":false,"RepeatMode":"RepeatNone"}},'
'"AccessToken":"4b19180cf02748f7b95c7e8e76562fc8",'
'"ServerId":"1efa5077976bfa92bc71652404f646ec"}')
responses.add(responses.POST,
('http://localhost:8096'
'/Users/AuthenticateByName'),
body=body,
status=200,
content_type='application/json')
headers = {
'x-emby-authorization': (
'MediaBrowser '
'UserId="e8837bc1-ad67-520e-8cd2-f629e3155721", '
'Client="other", '
'Device="beets", '
'DeviceId="beets", '
'Version="0.0.0"'
)
}
auth_data = {
'username': 'username',
'password': '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8',
'passwordMd5': '5f4dcc3b5aa765d61d8327deb882cf99'
}
self.assertEqual(
embyupdate.get_token('http://localhost', 8096, headers, auth_data),
'4b19180cf02748f7b95c7e8e76562fc8')
@responses.activate
def test_get_user(self):
body = ('[{"Name":"username",'
'"ServerId":"1efa5077976bfa92bc71652404f646ec",'
'"Id":"2ec276a2642e54a19b612b9418a8bd3b","HasPassword":true,'
'"HasConfiguredPassword":true,'
'"HasConfiguredEasyPassword":false,'
'"LastLoginDate":"2015-11-09T08:35:03.6357440Z",'
'"LastActivityDate":"2015-11-09T08:42:39.3693220Z",'
'"Configuration":{"AudioLanguagePreference":"",'
'"PlayDefaultAudioTrack":true,"SubtitleLanguagePreference":"",'
'"DisplayMissingEpisodes":false,'
'"DisplayUnairedEpisodes":false,'
'"GroupMoviesIntoBoxSets":false,'
'"DisplayChannelsWithinViews":[],'
'"ExcludeFoldersFromGrouping":[],"GroupedFolders":[],'
'"SubtitleMode":"Default","DisplayCollectionsView":true,'
'"DisplayFoldersView":false,"EnableLocalPassword":false,'
'"OrderedViews":[],"IncludeTrailersInSuggestions":true,'
'"EnableCinemaMode":true,"LatestItemsExcludes":[],'
'"PlainFolderViews":[],"HidePlayedInLatest":true,'
'"DisplayChannelsInline":false},'
'"Policy":{"IsAdministrator":true,"IsHidden":false,'
'"IsDisabled":false,"BlockedTags":[],'
'"EnableUserPreferenceAccess":true,"AccessSchedules":[],'
'"BlockUnratedItems":[],'
'"EnableRemoteControlOfOtherUsers":false,'
'"EnableSharedDeviceControl":true,'
'"EnableLiveTvManagement":true,"EnableLiveTvAccess":true,'
'"EnableMediaPlayback":true,'
'"EnableAudioPlaybackTranscoding":true,'
'"EnableVideoPlaybackTranscoding":true,'
'"EnableContentDeletion":false,'
'"EnableContentDownloading":true,'
'"EnableSync":true,"EnableSyncTranscoding":true,'
'"EnabledDevices":[],"EnableAllDevices":true,'
'"EnabledChannels":[],"EnableAllChannels":true,'
'"EnabledFolders":[],"EnableAllFolders":true,'
'"InvalidLoginAttemptCount":0,"EnablePublicSharing":true}}]')
responses.add(responses.GET,
'http://localhost:8096/Users/Public',
body=body,
status=200,
content_type='application/json')
response = embyupdate.get_user('http://localhost', 8096, 'username')
self.assertEqual(response[0]['Id'],
'2ec276a2642e54a19b612b9418a8bd3b')
self.assertEqual(response[0]['Name'],
'username')
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/test_plugins.py 0000644 0000765 0000024 00000050133 13031013626 017636 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Thomas Scholtes.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
from __future__ import division, absolute_import, print_function
import os
from mock import patch, Mock, ANY
import shutil
import itertools
import unittest
from beets.importer import SingletonImportTask, SentinelImportTask, \
ArchiveImportTask, action
from beets import plugins, config, ui
from beets.library import Item
from beets.dbcore import types
from beets.mediafile import MediaFile
from beets.util import displayable_path, bytestring_path, syspath
from test.test_importer import ImportHelper, AutotagStub
from test.test_ui_importer import TerminalImportSessionSetup
from test._common import RSRC
from test import helper
class TestHelper(helper.TestHelper):
def setup_plugin_loader(self):
# FIXME the mocking code is horrific, but this is the lowest and
# earliest level of the plugin mechanism we can hook into.
self.load_plugins()
self._plugin_loader_patch = patch('beets.plugins.load_plugins')
self._plugin_classes = set()
load_plugins = self._plugin_loader_patch.start()
def myload(names=()):
plugins._classes.update(self._plugin_classes)
load_plugins.side_effect = myload
self.setup_beets()
def teardown_plugin_loader(self):
self._plugin_loader_patch.stop()
self.unload_plugins()
def register_plugin(self, plugin_class):
self._plugin_classes.add(plugin_class)
class ItemTypesTest(unittest.TestCase, TestHelper):
def setUp(self):
self.setup_plugin_loader()
def tearDown(self):
self.teardown_plugin_loader()
self.teardown_beets()
def test_flex_field_type(self):
class RatingPlugin(plugins.BeetsPlugin):
item_types = {'rating': types.Float()}
self.register_plugin(RatingPlugin)
self.config['plugins'] = 'rating'
item = Item(path=u'apath', artist=u'aaa')
item.add(self.lib)
# Do not match unset values
out = self.run_with_output(u'ls', u'rating:1..3')
self.assertNotIn(u'aaa', out)
self.run_command(u'modify', u'rating=2', u'--yes')
# Match in range
out = self.run_with_output(u'ls', u'rating:1..3')
self.assertIn(u'aaa', out)
# Don't match out of range
out = self.run_with_output(u'ls', u'rating:3..5')
self.assertNotIn(u'aaa', out)
class ItemWriteTest(unittest.TestCase, TestHelper):
def setUp(self):
self.setup_plugin_loader()
self.setup_beets()
class EventListenerPlugin(plugins.BeetsPlugin):
pass
self.event_listener_plugin = EventListenerPlugin()
self.register_plugin(EventListenerPlugin)
def tearDown(self):
self.teardown_plugin_loader()
self.teardown_beets()
def test_change_tags(self):
def on_write(item=None, path=None, tags=None):
if tags['artist'] == u'XXX':
tags['artist'] = u'YYY'
self.register_listener('write', on_write)
item = self.add_item_fixture(artist=u'XXX')
item.write()
mediafile = MediaFile(syspath(item.path))
self.assertEqual(mediafile.artist, u'YYY')
def register_listener(self, event, func):
self.event_listener_plugin.register_listener(event, func)
class ItemTypeConflictTest(unittest.TestCase, TestHelper):
def setUp(self):
self.setup_plugin_loader()
self.setup_beets()
def tearDown(self):
self.teardown_plugin_loader()
self.teardown_beets()
def test_mismatch(self):
class EventListenerPlugin(plugins.BeetsPlugin):
item_types = {'duplicate': types.INTEGER}
class AdventListenerPlugin(plugins.BeetsPlugin):
item_types = {'duplicate': types.FLOAT}
self.event_listener_plugin = EventListenerPlugin
self.advent_listener_plugin = AdventListenerPlugin
self.register_plugin(EventListenerPlugin)
self.register_plugin(AdventListenerPlugin)
self.assertRaises(plugins.PluginConflictException,
plugins.types, Item
)
def test_match(self):
class EventListenerPlugin(plugins.BeetsPlugin):
item_types = {'duplicate': types.INTEGER}
class AdventListenerPlugin(plugins.BeetsPlugin):
item_types = {'duplicate': types.INTEGER}
self.event_listener_plugin = EventListenerPlugin
self.advent_listener_plugin = AdventListenerPlugin
self.register_plugin(EventListenerPlugin)
self.register_plugin(AdventListenerPlugin)
self.assertNotEqual(None, plugins.types(Item))
class EventsTest(unittest.TestCase, ImportHelper, TestHelper):
def setUp(self):
self.setup_plugin_loader()
self.setup_beets()
self.__create_import_dir(2)
config['import']['pretend'] = True
def tearDown(self):
self.teardown_plugin_loader()
self.teardown_beets()
def __copy_file(self, dest_path, metadata):
# Copy files
resource_path = os.path.join(RSRC, b'full.mp3')
shutil.copy(resource_path, dest_path)
medium = MediaFile(dest_path)
# Set metadata
for attr in metadata:
setattr(medium, attr, metadata[attr])
medium.save()
def __create_import_dir(self, count):
self.import_dir = os.path.join(self.temp_dir, b'testsrcdir')
if os.path.isdir(self.import_dir):
shutil.rmtree(self.import_dir)
self.album_path = os.path.join(self.import_dir, b'album')
os.makedirs(self.album_path)
metadata = {
'artist': u'Tag Artist',
'album': u'Tag Album',
'albumartist': None,
'mb_trackid': None,
'mb_albumid': None,
'comp': None
}
self.file_paths = []
for i in range(count):
metadata['track'] = i + 1
metadata['title'] = u'Tag Title Album %d' % (i + 1)
track_file = bytestring_path('%02d - track.mp3' % (i + 1))
dest_path = os.path.join(self.album_path, track_file)
self.__copy_file(dest_path, metadata)
self.file_paths.append(dest_path)
def test_import_task_created(self):
import_files = [self.import_dir]
self._setup_import_session(singletons=False)
self.importer.paths = import_files
with helper.capture_log() as logs:
self.importer.run()
self.unload_plugins()
# Exactly one event should have been imported (for the album).
# Sentinels do not get emitted.
self.assertEqual(logs.count(u'Sending event: import_task_created'), 1)
logs = [line for line in logs if not line.startswith(
u'Sending event:')]
self.assertEqual(logs, [
u'Album: {0}'.format(displayable_path(
os.path.join(self.import_dir, b'album'))),
u' {0}'.format(displayable_path(self.file_paths[0])),
u' {0}'.format(displayable_path(self.file_paths[1])),
])
def test_import_task_created_with_plugin(self):
class ToSingletonPlugin(plugins.BeetsPlugin):
def __init__(self):
super(ToSingletonPlugin, self).__init__()
self.register_listener('import_task_created',
self.import_task_created_event)
def import_task_created_event(self, session, task):
if isinstance(task, SingletonImportTask) \
or isinstance(task, SentinelImportTask)\
or isinstance(task, ArchiveImportTask):
return task
new_tasks = []
for item in task.items:
new_tasks.append(SingletonImportTask(task.toppath, item))
return new_tasks
to_singleton_plugin = ToSingletonPlugin
self.register_plugin(to_singleton_plugin)
import_files = [self.import_dir]
self._setup_import_session(singletons=False)
self.importer.paths = import_files
with helper.capture_log() as logs:
self.importer.run()
self.unload_plugins()
# Exactly one event should have been imported (for the album).
# Sentinels do not get emitted.
self.assertEqual(logs.count(u'Sending event: import_task_created'), 1)
logs = [line for line in logs if not line.startswith(
u'Sending event:')]
self.assertEqual(logs, [
u'Singleton: {0}'.format(displayable_path(self.file_paths[0])),
u'Singleton: {0}'.format(displayable_path(self.file_paths[1])),
])
class HelpersTest(unittest.TestCase):
def test_sanitize_choices(self):
self.assertEqual(
plugins.sanitize_choices([u'A', u'Z'], (u'A', u'B')), [u'A'])
self.assertEqual(
plugins.sanitize_choices([u'A', u'A'], (u'A')), [u'A'])
self.assertEqual(
plugins.sanitize_choices([u'D', u'*', u'A'],
(u'A', u'B', u'C', u'D')),
[u'D', u'B', u'C', u'A'])
class ListenersTest(unittest.TestCase, TestHelper):
def setUp(self):
self.setup_plugin_loader()
def tearDown(self):
self.teardown_plugin_loader()
self.teardown_beets()
def test_register(self):
class DummyPlugin(plugins.BeetsPlugin):
def __init__(self):
super(DummyPlugin, self).__init__()
self.register_listener('cli_exit', self.dummy)
self.register_listener('cli_exit', self.dummy)
def dummy(self):
pass
d = DummyPlugin()
self.assertEqual(DummyPlugin._raw_listeners['cli_exit'], [d.dummy])
d2 = DummyPlugin()
self.assertEqual(DummyPlugin._raw_listeners['cli_exit'],
[d.dummy, d2.dummy])
d.register_listener('cli_exit', d2.dummy)
self.assertEqual(DummyPlugin._raw_listeners['cli_exit'],
[d.dummy, d2.dummy])
@patch('beets.plugins.find_plugins')
@patch('beets.plugins.inspect')
def test_events_called(self, mock_inspect, mock_find_plugins):
mock_inspect.getargspec.return_value = None
class DummyPlugin(plugins.BeetsPlugin):
def __init__(self):
super(DummyPlugin, self).__init__()
self.foo = Mock(__name__='foo')
self.register_listener('event_foo', self.foo)
self.bar = Mock(__name__='bar')
self.register_listener('event_bar', self.bar)
d = DummyPlugin()
mock_find_plugins.return_value = d,
plugins.send('event')
d.foo.assert_has_calls([])
d.bar.assert_has_calls([])
plugins.send('event_foo', var=u"tagada")
d.foo.assert_called_once_with(var=u"tagada")
d.bar.assert_has_calls([])
@patch('beets.plugins.find_plugins')
def test_listener_params(self, mock_find_plugins):
test = self
class DummyPlugin(plugins.BeetsPlugin):
def __init__(self):
super(DummyPlugin, self).__init__()
for i in itertools.count(1):
try:
meth = getattr(self, 'dummy{0}'.format(i))
except AttributeError:
break
self.register_listener('event{0}'.format(i), meth)
def dummy1(self, foo):
test.assertEqual(foo, 5)
def dummy2(self, foo=None):
test.assertEqual(foo, 5)
def dummy3(self):
# argument cut off
pass
def dummy4(self, bar=None):
# argument cut off
pass
def dummy5(self, bar):
test.assertFalse(True)
# more complex exmaples
def dummy6(self, foo, bar=None):
test.assertEqual(foo, 5)
test.assertEqual(bar, None)
def dummy7(self, foo, **kwargs):
test.assertEqual(foo, 5)
test.assertEqual(kwargs, {})
def dummy8(self, foo, bar, **kwargs):
test.assertFalse(True)
def dummy9(self, **kwargs):
test.assertEqual(kwargs, {"foo": 5})
d = DummyPlugin()
mock_find_plugins.return_value = d,
plugins.send('event1', foo=5)
plugins.send('event2', foo=5)
plugins.send('event3', foo=5)
plugins.send('event4', foo=5)
with self.assertRaises(TypeError):
plugins.send('event5', foo=5)
plugins.send('event6', foo=5)
plugins.send('event7', foo=5)
with self.assertRaises(TypeError):
plugins.send('event8', foo=5)
plugins.send('event9', foo=5)
class PromptChoicesTest(TerminalImportSessionSetup, unittest.TestCase,
ImportHelper, TestHelper):
def setUp(self):
self.setup_plugin_loader()
self.setup_beets()
self._create_import_dir(3)
self._setup_import_session()
self.matcher = AutotagStub().install()
# keep track of ui.input_option() calls
self.input_options_patcher = patch('beets.ui.input_options',
side_effect=ui.input_options)
self.mock_input_options = self.input_options_patcher.start()
def tearDown(self):
self.input_options_patcher.stop()
self.teardown_plugin_loader()
self.teardown_beets()
self.matcher.restore()
def test_plugin_choices_in_ui_input_options_album(self):
"""Test the presence of plugin choices on the prompt (album)."""
class DummyPlugin(plugins.BeetsPlugin):
def __init__(self):
super(DummyPlugin, self).__init__()
self.register_listener('before_choose_candidate',
self.return_choices)
def return_choices(self, session, task):
return [ui.commands.PromptChoice('f', u'Foo', None),
ui.commands.PromptChoice('r', u'baR', None)]
self.register_plugin(DummyPlugin)
# Default options + extra choices by the plugin ('Foo', 'Bar')
opts = (u'Apply', u'More candidates', u'Skip', u'Use as-is',
u'as Tracks', u'Group albums', u'Enter search',
u'enter Id', u'aBort') + (u'Foo', u'baR')
self.importer.add_choice(action.SKIP)
self.importer.run()
self.mock_input_options.assert_called_once_with(opts, default='a',
require=ANY)
def test_plugin_choices_in_ui_input_options_singleton(self):
"""Test the presence of plugin choices on the prompt (singleton)."""
class DummyPlugin(plugins.BeetsPlugin):
def __init__(self):
super(DummyPlugin, self).__init__()
self.register_listener('before_choose_candidate',
self.return_choices)
def return_choices(self, session, task):
return [ui.commands.PromptChoice('f', u'Foo', None),
ui.commands.PromptChoice('r', u'baR', None)]
self.register_plugin(DummyPlugin)
# Default options + extra choices by the plugin ('Foo', 'Bar')
opts = (u'Apply', u'More candidates', u'Skip', u'Use as-is',
u'Enter search',
u'enter Id', u'aBort') + (u'Foo', u'baR')
config['import']['singletons'] = True
self.importer.add_choice(action.SKIP)
self.importer.run()
self.mock_input_options.assert_called_with(opts, default='a',
require=ANY)
def test_choices_conflicts(self):
"""Test the short letter conflict solving."""
class DummyPlugin(plugins.BeetsPlugin):
def __init__(self):
super(DummyPlugin, self).__init__()
self.register_listener('before_choose_candidate',
self.return_choices)
def return_choices(self, session, task):
return [ui.commands.PromptChoice('a', u'A foo', None), # dupe
ui.commands.PromptChoice('z', u'baZ', None), # ok
ui.commands.PromptChoice('z', u'Zupe', None), # dupe
ui.commands.PromptChoice('z', u'Zoo', None)] # dupe
self.register_plugin(DummyPlugin)
# Default options + not dupe extra choices by the plugin ('baZ')
opts = (u'Apply', u'More candidates', u'Skip', u'Use as-is',
u'as Tracks', u'Group albums', u'Enter search',
u'enter Id', u'aBort') + (u'baZ',)
self.importer.add_choice(action.SKIP)
self.importer.run()
self.mock_input_options.assert_called_once_with(opts, default='a',
require=ANY)
def test_plugin_callback(self):
"""Test that plugin callbacks are being called upon user choice."""
class DummyPlugin(plugins.BeetsPlugin):
def __init__(self):
super(DummyPlugin, self).__init__()
self.register_listener('before_choose_candidate',
self.return_choices)
def return_choices(self, session, task):
return [ui.commands.PromptChoice('f', u'Foo', self.foo)]
def foo(self, session, task):
pass
self.register_plugin(DummyPlugin)
# Default options + extra choices by the plugin ('Foo', 'Bar')
opts = (u'Apply', u'More candidates', u'Skip', u'Use as-is',
u'as Tracks', u'Group albums', u'Enter search',
u'enter Id', u'aBort') + (u'Foo',)
# DummyPlugin.foo() should be called once
with patch.object(DummyPlugin, 'foo', autospec=True) as mock_foo:
with helper.control_stdin('\n'.join(['f', 's'])):
self.importer.run()
self.assertEqual(mock_foo.call_count, 1)
# input_options should be called twice, as foo() returns None
self.assertEqual(self.mock_input_options.call_count, 2)
self.mock_input_options.assert_called_with(opts, default='a',
require=ANY)
def test_plugin_callback_return(self):
"""Test that plugin callbacks that return a value exit the loop."""
class DummyPlugin(plugins.BeetsPlugin):
def __init__(self):
super(DummyPlugin, self).__init__()
self.register_listener('before_choose_candidate',
self.return_choices)
def return_choices(self, session, task):
return [ui.commands.PromptChoice('f', u'Foo', self.foo)]
def foo(self, session, task):
return action.SKIP
self.register_plugin(DummyPlugin)
# Default options + extra choices by the plugin ('Foo', 'Bar')
opts = (u'Apply', u'More candidates', u'Skip', u'Use as-is',
u'as Tracks', u'Group albums', u'Enter search',
u'enter Id', u'aBort') + (u'Foo',)
# DummyPlugin.foo() should be called once
with helper.control_stdin('f\n'):
self.importer.run()
# input_options should be called once, as foo() returns SKIP
self.mock_input_options.assert_called_once_with(opts, default='a',
require=ANY)
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/test_importfeeds.py 0000644 0000765 0000024 00000004246 13025125203 020500 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
from __future__ import division, absolute_import, print_function
import os
import os.path
import tempfile
import shutil
import unittest
from beets import config
from beets.library import Item, Album, Library
from beetsplug.importfeeds import ImportFeedsPlugin
class ImportfeedsTestTest(unittest.TestCase):
def setUp(self):
config.clear()
config.read(user=False)
self.importfeeds = ImportFeedsPlugin()
self.lib = Library(':memory:')
self.feeds_dir = tempfile.mkdtemp()
config['importfeeds']['dir'] = self.feeds_dir
def tearDown(self):
shutil.rmtree(self.feeds_dir)
def test_multi_format_album_playlist(self):
config['importfeeds']['formats'] = 'm3u_multi'
album = Album(album='album/name', id=1)
item_path = os.path.join('path', 'to', 'item')
item = Item(title='song', album_id=1, path=item_path)
self.lib.add(album)
self.lib.add(item)
self.importfeeds.album_imported(self.lib, album)
playlist_path = os.path.join(self.feeds_dir,
os.listdir(self.feeds_dir)[0])
self.assertTrue(playlist_path.endswith('album_name.m3u'))
with open(playlist_path) as playlist:
self.assertIn(item_path, playlist.read())
def test_playlist_in_subdir(self):
config['importfeeds']['formats'] = 'm3u'
config['importfeeds']['m3u_name'] = \
os.path.join('subdir', 'imported.m3u')
album = Album(album='album/name', id=1)
item_path = os.path.join('path', 'to', 'item')
item = Item(title='song', album_id=1, path=item_path)
self.lib.add(album)
self.lib.add(item)
self.importfeeds.album_imported(self.lib, album)
playlist = os.path.join(self.feeds_dir,
config['importfeeds']['m3u_name'].get())
playlist_subdir = os.path.dirname(playlist)
self.assertTrue(os.path.isdir(playlist_subdir))
self.assertTrue(os.path.isfile(playlist))
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/test_thumbnails.py 0000644 0000765 0000024 00000025713 13120341455 020334 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Bruno Cauet
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
from __future__ import division, absolute_import, print_function
import os.path
from mock import Mock, patch, call
from tempfile import mkdtemp
from shutil import rmtree
import unittest
from test.helper import TestHelper
from beets.util import bytestring_path
from beetsplug.thumbnails import (ThumbnailsPlugin, NORMAL_DIR, LARGE_DIR,
write_metadata_im, write_metadata_pil,
PathlibURI, GioURI)
class ThumbnailsTest(unittest.TestCase, TestHelper):
def setUp(self):
self.setup_beets()
def tearDown(self):
self.teardown_beets()
@patch('beetsplug.thumbnails.util')
def test_write_metadata_im(self, mock_util):
metadata = {"a": u"A", "b": u"B"}
write_metadata_im("foo", metadata)
try:
command = u"convert foo -set a A -set b B foo".split(' ')
mock_util.command_output.assert_called_once_with(command)
except AssertionError:
command = u"convert foo -set b B -set a A foo".split(' ')
mock_util.command_output.assert_called_once_with(command)
@patch('beetsplug.thumbnails.ThumbnailsPlugin._check_local_ok')
@patch('beetsplug.thumbnails.os.stat')
def test_add_tags(self, mock_stat, _):
plugin = ThumbnailsPlugin()
plugin.write_metadata = Mock()
plugin.get_uri = Mock(side_effect={b"/path/to/cover":
"COVER_URI"}.__getitem__)
album = Mock(artpath=b"/path/to/cover")
mock_stat.return_value.st_mtime = 12345
plugin.add_tags(album, b"/path/to/thumbnail")
metadata = {"Thumb::URI": "COVER_URI",
"Thumb::MTime": u"12345"}
plugin.write_metadata.assert_called_once_with(b"/path/to/thumbnail",
metadata)
mock_stat.assert_called_once_with(album.artpath)
@patch('beetsplug.thumbnails.os')
@patch('beetsplug.thumbnails.ArtResizer')
@patch('beetsplug.thumbnails.get_im_version')
@patch('beetsplug.thumbnails.get_pil_version')
@patch('beetsplug.thumbnails.GioURI')
def test_check_local_ok(self, mock_giouri, mock_pil, mock_im,
mock_artresizer, mock_os):
# test local resizing capability
mock_artresizer.shared.local = False
plugin = ThumbnailsPlugin()
self.assertFalse(plugin._check_local_ok())
# test dirs creation
mock_artresizer.shared.local = True
def exists(path):
if path == NORMAL_DIR:
return False
if path == LARGE_DIR:
return True
raise ValueError(u"unexpected path {0!r}".format(path))
mock_os.path.exists = exists
plugin = ThumbnailsPlugin()
mock_os.makedirs.assert_called_once_with(NORMAL_DIR)
self.assertTrue(plugin._check_local_ok())
# test metadata writer function
mock_os.path.exists = lambda _: True
mock_pil.return_value = False
mock_im.return_value = False
with self.assertRaises(AssertionError):
ThumbnailsPlugin()
mock_pil.return_value = True
self.assertEqual(ThumbnailsPlugin().write_metadata, write_metadata_pil)
mock_im.return_value = True
self.assertEqual(ThumbnailsPlugin().write_metadata, write_metadata_im)
mock_pil.return_value = False
self.assertEqual(ThumbnailsPlugin().write_metadata, write_metadata_im)
self.assertTrue(ThumbnailsPlugin()._check_local_ok())
# test URI getter function
giouri_inst = mock_giouri.return_value
giouri_inst.available = True
self.assertEqual(ThumbnailsPlugin().get_uri, giouri_inst.uri)
giouri_inst.available = False
self.assertEqual(ThumbnailsPlugin().get_uri.__self__.__class__,
PathlibURI)
@patch('beetsplug.thumbnails.ThumbnailsPlugin._check_local_ok')
@patch('beetsplug.thumbnails.ArtResizer')
@patch('beetsplug.thumbnails.util')
@patch('beetsplug.thumbnails.os')
@patch('beetsplug.thumbnails.shutil')
def test_make_cover_thumbnail(self, mock_shutils, mock_os, mock_util,
mock_artresizer, _):
thumbnail_dir = os.path.normpath(b"/thumbnail/dir")
md5_file = os.path.join(thumbnail_dir, b"md5")
path_to_art = os.path.normpath(b"/path/to/art")
mock_os.path.join = os.path.join # don't mock that function
plugin = ThumbnailsPlugin()
plugin.add_tags = Mock()
album = Mock(artpath=path_to_art)
mock_util.syspath.side_effect = lambda x: x
plugin.thumbnail_file_name = Mock(return_value=b'md5')
mock_os.path.exists.return_value = False
def os_stat(target):
if target == md5_file:
return Mock(st_mtime=1)
elif target == path_to_art:
return Mock(st_mtime=2)
else:
raise ValueError(u"invalid target {0}".format(target))
mock_os.stat.side_effect = os_stat
plugin.make_cover_thumbnail(album, 12345, thumbnail_dir)
mock_os.path.exists.assert_called_once_with(md5_file)
mock_os.stat.has_calls([call(md5_file), call(path_to_art)],
any_order=True)
resize = mock_artresizer.shared.resize
resize.assert_called_once_with(12345, path_to_art, md5_file)
plugin.add_tags.assert_called_once_with(album, resize.return_value)
mock_shutils.move.assert_called_once_with(resize.return_value,
md5_file)
# now test with recent thumbnail & with force
mock_os.path.exists.return_value = True
plugin.force = False
resize.reset_mock()
def os_stat(target):
if target == md5_file:
return Mock(st_mtime=3)
elif target == path_to_art:
return Mock(st_mtime=2)
else:
raise ValueError(u"invalid target {0}".format(target))
mock_os.stat.side_effect = os_stat
plugin.make_cover_thumbnail(album, 12345, thumbnail_dir)
self.assertEqual(resize.call_count, 0)
# and with force
plugin.config['force'] = True
plugin.make_cover_thumbnail(album, 12345, thumbnail_dir)
resize.assert_called_once_with(12345, path_to_art, md5_file)
@patch('beetsplug.thumbnails.ThumbnailsPlugin._check_local_ok')
def test_make_dolphin_cover_thumbnail(self, _):
plugin = ThumbnailsPlugin()
tmp = bytestring_path(mkdtemp())
album = Mock(path=tmp,
artpath=os.path.join(tmp, b"cover.jpg"))
plugin.make_dolphin_cover_thumbnail(album)
with open(os.path.join(tmp, b".directory"), "rb") as f:
self.assertEqual(
f.read().splitlines(),
[b"[Desktop Entry]", b"Icon=./cover.jpg"]
)
# not rewritten when it already exists (yup that's a big limitation)
album.artpath = b"/my/awesome/art.tiff"
plugin.make_dolphin_cover_thumbnail(album)
with open(os.path.join(tmp, b".directory"), "rb") as f:
self.assertEqual(
f.read().splitlines(),
[b"[Desktop Entry]", b"Icon=./cover.jpg"]
)
rmtree(tmp)
@patch('beetsplug.thumbnails.ThumbnailsPlugin._check_local_ok')
@patch('beetsplug.thumbnails.ArtResizer')
def test_process_album(self, mock_artresizer, _):
get_size = mock_artresizer.shared.get_size
plugin = ThumbnailsPlugin()
make_cover = plugin.make_cover_thumbnail = Mock(return_value=True)
make_dolphin = plugin.make_dolphin_cover_thumbnail = Mock()
# no art
album = Mock(artpath=None)
plugin.process_album(album)
self.assertEqual(get_size.call_count, 0)
self.assertEqual(make_dolphin.call_count, 0)
# cannot get art size
album.artpath = b"/path/to/art"
get_size.return_value = None
plugin.process_album(album)
get_size.assert_called_once_with(b"/path/to/art")
self.assertEqual(make_cover.call_count, 0)
# dolphin tests
plugin.config['dolphin'] = False
plugin.process_album(album)
self.assertEqual(make_dolphin.call_count, 0)
plugin.config['dolphin'] = True
plugin.process_album(album)
make_dolphin.assert_called_once_with(album)
# small art
get_size.return_value = 200, 200
plugin.process_album(album)
make_cover.assert_called_once_with(album, 128, NORMAL_DIR)
# big art
make_cover.reset_mock()
get_size.return_value = 500, 500
plugin.process_album(album)
make_cover.has_calls([call(album, 128, NORMAL_DIR),
call(album, 256, LARGE_DIR)], any_order=True)
@patch('beetsplug.thumbnails.ThumbnailsPlugin._check_local_ok')
@patch('beetsplug.thumbnails.decargs')
def test_invokations(self, mock_decargs, _):
plugin = ThumbnailsPlugin()
plugin.process_album = Mock()
album = Mock()
plugin.process_album.reset_mock()
lib = Mock()
album2 = Mock()
lib.albums.return_value = [album, album2]
plugin.process_query(lib, Mock(), None)
lib.albums.assert_called_once_with(mock_decargs.return_value)
plugin.process_album.has_calls([call(album), call(album2)],
any_order=True)
@patch('beetsplug.thumbnails.BaseDirectory')
def test_thumbnail_file_name(self, mock_basedir):
plug = ThumbnailsPlugin()
plug.get_uri = Mock(return_value=u"file:///my/uri")
self.assertEqual(plug.thumbnail_file_name(b'idontcare'),
b"9488f5797fbe12ffb316d607dfd93d04.png")
def test_uri(self):
gio = GioURI()
if not gio.available:
self.skipTest(u"GIO library not found")
self.assertEqual(gio.uri(u"/foo"), u"file:///") # silent fail
self.assertEqual(gio.uri(b"/foo"), u"file:///foo")
self.assertEqual(gio.uri(b"/foo!"), u"file:///foo!")
self.assertEqual(
gio.uri(b'/music/\xec\x8b\xb8\xec\x9d\xb4'),
u'file:///music/%EC%8B%B8%EC%9D%B4')
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/test_ihate.py 0000644 0000765 0000024 00000003432 13025125203 017245 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
"""Tests for the 'ihate' plugin"""
from __future__ import division, absolute_import, print_function
import unittest
from beets import importer
from beets.library import Item
from beetsplug.ihate import IHatePlugin
class IHatePluginTest(unittest.TestCase):
def test_hate(self):
match_pattern = {}
test_item = Item(
genre=u'TestGenre',
album=u'TestAlbum',
artist=u'TestArtist')
task = importer.SingletonImportTask(None, test_item)
# Empty query should let it pass.
self.assertFalse(IHatePlugin.do_i_hate_this(task, match_pattern))
# 1 query match.
match_pattern = [u"artist:bad_artist", u"artist:TestArtist"]
self.assertTrue(IHatePlugin.do_i_hate_this(task, match_pattern))
# 2 query matches, either should trigger.
match_pattern = [u"album:test", u"artist:testartist"]
self.assertTrue(IHatePlugin.do_i_hate_this(task, match_pattern))
# Query is blocked by AND clause.
match_pattern = [u"album:notthis genre:testgenre"]
self.assertFalse(IHatePlugin.do_i_hate_this(task, match_pattern))
# Both queries are blocked by AND clause with unmatched condition.
match_pattern = [u"album:notthis genre:testgenre",
u"artist:testartist album:notthis"]
self.assertFalse(IHatePlugin.do_i_hate_this(task, match_pattern))
# Only one query should fire.
match_pattern = [u"album:testalbum genre:testgenre",
u"artist:testartist album:notthis"]
self.assertTrue(IHatePlugin.do_i_hate_this(task, match_pattern))
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/test_config_command.py 0000644 0000765 0000024 00000011333 13025125203 021115 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
from __future__ import division, absolute_import, print_function
import os
import yaml
from mock import patch
from tempfile import mkdtemp
from shutil import rmtree
import unittest
from beets import ui
from beets import config
from test.helper import TestHelper
from beets.library import Library
import six
class ConfigCommandTest(unittest.TestCase, TestHelper):
def setUp(self):
self.lib = Library(':memory:')
self.temp_dir = mkdtemp()
if 'EDITOR' in os.environ:
del os.environ['EDITOR']
os.environ['BEETSDIR'] = self.temp_dir
self.config_path = os.path.join(self.temp_dir, 'config.yaml')
with open(self.config_path, 'w') as file:
file.write('library: lib\n')
file.write('option: value\n')
file.write('password: password_value')
self.cli_config_path = os.path.join(self.temp_dir, 'cli_config.yaml')
with open(self.cli_config_path, 'w') as file:
file.write('option: cli overwrite')
config.clear()
config['password'].redact = True
config._materialized = False
def tearDown(self):
rmtree(self.temp_dir)
def _run_with_yaml_output(self, *args):
output = self.run_with_output(*args)
return yaml.load(output)
def test_show_user_config(self):
output = self._run_with_yaml_output('config', '-c')
self.assertEqual(output['option'], 'value')
self.assertEqual(output['password'], 'password_value')
def test_show_user_config_with_defaults(self):
output = self._run_with_yaml_output('config', '-dc')
self.assertEqual(output['option'], 'value')
self.assertEqual(output['password'], 'password_value')
self.assertEqual(output['library'], 'lib')
self.assertEqual(output['import']['timid'], False)
def test_show_user_config_with_cli(self):
output = self._run_with_yaml_output('--config', self.cli_config_path,
'config')
self.assertEqual(output['library'], 'lib')
self.assertEqual(output['option'], 'cli overwrite')
def test_show_redacted_user_config(self):
output = self._run_with_yaml_output('config')
self.assertEqual(output['option'], 'value')
self.assertEqual(output['password'], 'REDACTED')
def test_show_redacted_user_config_with_defaults(self):
output = self._run_with_yaml_output('config', '-d')
self.assertEqual(output['option'], 'value')
self.assertEqual(output['password'], 'REDACTED')
self.assertEqual(output['import']['timid'], False)
def test_config_paths(self):
output = self.run_with_output('config', '-p')
paths = output.split('\n')
self.assertEqual(len(paths), 2)
self.assertEqual(paths[0], self.config_path)
def test_config_paths_with_cli(self):
output = self.run_with_output('--config', self.cli_config_path,
'config', '-p')
paths = output.split('\n')
self.assertEqual(len(paths), 3)
self.assertEqual(paths[0], self.cli_config_path)
def test_edit_config_with_editor_env(self):
os.environ['EDITOR'] = 'myeditor'
with patch('os.execlp') as execlp:
self.run_command('config', '-e')
execlp.assert_called_once_with(
'myeditor', 'myeditor', self.config_path)
def test_edit_config_with_automatic_open(self):
with patch('beets.util.open_anything') as open:
open.return_value = 'please_open'
with patch('os.execlp') as execlp:
self.run_command('config', '-e')
execlp.assert_called_once_with(
'please_open', 'please_open', self.config_path)
def test_config_editor_not_found(self):
with self.assertRaises(ui.UserError) as user_error:
with patch('os.execlp') as execlp:
execlp.side_effect = OSError('here is problem')
self.run_command('config', '-e')
self.assertIn('Could not edit configuration',
six.text_type(user_error.exception))
self.assertIn('here is problem', six.text_type(user_error.exception))
def test_edit_invalid_config_file(self):
with open(self.config_path, 'w') as file:
file.write('invalid: [')
config.clear()
config._materialized = False
os.environ['EDITOR'] = 'myeditor'
with patch('os.execlp') as execlp:
self.run_command('config', '-e')
execlp.assert_called_once_with(
'myeditor', 'myeditor', self.config_path)
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/test_keyfinder.py 0000644 0000765 0000024 00000004732 13025125203 020137 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Thomas Scholtes.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
from __future__ import division, absolute_import, print_function
from mock import patch
import unittest
from test.helper import TestHelper
from beets.library import Item
from beets import util
@patch('beets.util.command_output')
class KeyFinderTest(unittest.TestCase, TestHelper):
def setUp(self):
self.setup_beets()
self.load_plugins('keyfinder')
def tearDown(self):
self.teardown_beets()
self.unload_plugins()
def test_add_key(self, command_output):
item = Item(path='/file')
item.add(self.lib)
command_output.return_value = 'dbm'
self.run_command('keyfinder')
item.load()
self.assertEqual(item['initial_key'], 'C#m')
command_output.assert_called_with(
['KeyFinder', '-f', util.syspath(item.path)])
def test_add_key_on_import(self, command_output):
command_output.return_value = 'dbm'
importer = self.create_importer()
importer.run()
item = self.lib.items().get()
self.assertEqual(item['initial_key'], 'C#m')
def test_force_overwrite(self, command_output):
self.config['keyfinder']['overwrite'] = True
item = Item(path='/file', initial_key='F')
item.add(self.lib)
command_output.return_value = 'C#m'
self.run_command('keyfinder')
item.load()
self.assertEqual(item['initial_key'], 'C#m')
def test_do_not_overwrite(self, command_output):
item = Item(path='/file', initial_key='F')
item.add(self.lib)
command_output.return_value = 'dbm'
self.run_command('keyfinder')
item.load()
self.assertEqual(item['initial_key'], 'F')
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/testall.py 0000755 0000765 0000024 00000002425 13025125203 016570 0 ustar asampson staff 0000000 0000000 #!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
from __future__ import division, absolute_import, print_function
import os
import re
import sys
import unittest
pkgpath = os.path.dirname(__file__) or '.'
sys.path.append(pkgpath)
os.chdir(pkgpath)
def suite():
s = unittest.TestSuite()
# Get the suite() of every module in this directory beginning with
# "test_".
for fname in os.listdir(pkgpath):
match = re.match(r'(test_\S+)\.py$', fname)
if match:
modname = match.group(1)
s.addTest(__import__(modname).suite())
return s
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/test_zero.py 0000644 0000765 0000024 00000021471 13032602010 017127 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
"""Tests for the 'zero' plugin"""
from __future__ import division, absolute_import, print_function
import unittest
from test.helper import TestHelper, control_stdin
from beets.library import Item
from beetsplug.zero import ZeroPlugin
from beets.mediafile import MediaFile
from beets.util import syspath
class ZeroPluginTest(unittest.TestCase, TestHelper):
def setUp(self):
self.setup_beets()
self.config['zero'] = {
'fields': [],
'keep_fields': [],
'update_database': False,
}
def tearDown(self):
ZeroPlugin.listeners = None
self.teardown_beets()
self.unload_plugins()
def test_no_patterns(self):
self.config['zero']['fields'] = ['comments', 'month']
item = self.add_item_fixture(
comments=u'test comment',
title=u'Title',
month=1,
year=2000,
)
item.write()
self.load_plugins('zero')
item.write()
mf = MediaFile(syspath(item.path))
self.assertIsNone(mf.comments)
self.assertIsNone(mf.month)
self.assertEqual(mf.title, u'Title')
self.assertEqual(mf.year, 2000)
def test_pattern_match(self):
self.config['zero']['fields'] = ['comments']
self.config['zero']['comments'] = [u'encoded by']
item = self.add_item_fixture(comments=u'encoded by encoder')
item.write()
self.load_plugins('zero')
item.write()
mf = MediaFile(syspath(item.path))
self.assertIsNone(mf.comments)
def test_pattern_nomatch(self):
self.config['zero']['fields'] = ['comments']
self.config['zero']['comments'] = [u'encoded by']
item = self.add_item_fixture(comments=u'recorded at place')
item.write()
self.load_plugins('zero')
item.write()
mf = MediaFile(syspath(item.path))
self.assertEqual(mf.comments, u'recorded at place')
def test_do_not_change_database(self):
self.config['zero']['fields'] = ['year']
item = self.add_item_fixture(year=2000)
item.write()
self.load_plugins('zero')
item.write()
self.assertEqual(item['year'], 2000)
def test_change_database(self):
self.config['zero']['fields'] = ['year']
self.config['zero']['update_database'] = True
item = self.add_item_fixture(year=2000)
item.write()
self.load_plugins('zero')
item.write()
self.assertEqual(item['year'], 0)
def test_album_art(self):
self.config['zero']['fields'] = ['images']
path = self.create_mediafile_fixture(images=['jpg'])
item = Item.from_path(path)
self.load_plugins('zero')
item.write()
mf = MediaFile(syspath(path))
self.assertEqual(0, len(mf.images))
def test_auto_false(self):
self.config['zero']['fields'] = ['year']
self.config['zero']['update_database'] = True
self.config['zero']['auto'] = False
item = self.add_item_fixture(year=2000)
item.write()
self.load_plugins('zero')
item.write()
self.assertEqual(item['year'], 2000)
def test_subcommand_update_database_true(self):
item = self.add_item_fixture(
year=2016,
day=13,
month=3,
comments=u'test comment'
)
item.write()
item_id = item.id
self.config['zero']['fields'] = ['comments']
self.config['zero']['update_database'] = True
self.config['zero']['auto'] = False
self.load_plugins('zero')
with control_stdin('y'):
self.run_command('zero')
mf = MediaFile(syspath(item.path))
item = self.lib.get_item(item_id)
self.assertEqual(item['year'], 2016)
self.assertEqual(mf.year, 2016)
self.assertEqual(mf.comments, None)
self.assertEqual(item['comments'], u'')
def test_subcommand_update_database_false(self):
item = self.add_item_fixture(
year=2016,
day=13,
month=3,
comments=u'test comment'
)
item.write()
item_id = item.id
self.config['zero']['fields'] = ['comments']
self.config['zero']['update_database'] = False
self.config['zero']['auto'] = False
self.load_plugins('zero')
with control_stdin('y'):
self.run_command('zero')
mf = MediaFile(syspath(item.path))
item = self.lib.get_item(item_id)
self.assertEqual(item['year'], 2016)
self.assertEqual(mf.year, 2016)
self.assertEqual(item['comments'], u'test comment')
self.assertEqual(mf.comments, None)
def test_subcommand_query_include(self):
item = self.add_item_fixture(
year=2016,
day=13,
month=3,
comments=u'test comment'
)
item.write()
self.config['zero']['fields'] = ['comments']
self.config['zero']['update_database'] = False
self.config['zero']['auto'] = False
self.load_plugins('zero')
self.run_command('zero', 'year: 2016')
mf = MediaFile(syspath(item.path))
self.assertEqual(mf.year, 2016)
self.assertEqual(mf.comments, None)
def test_subcommand_query_exclude(self):
item = self.add_item_fixture(
year=2016,
day=13,
month=3,
comments=u'test comment'
)
item.write()
self.config['zero']['fields'] = ['comments']
self.config['zero']['update_database'] = False
self.config['zero']['auto'] = False
self.load_plugins('zero')
self.run_command('zero', 'year: 0000')
mf = MediaFile(syspath(item.path))
self.assertEqual(mf.year, 2016)
self.assertEqual(mf.comments, u'test comment')
def test_no_fields(self):
item = self.add_item_fixture(year=2016)
item.write()
mediafile = MediaFile(syspath(item.path))
self.assertEqual(mediafile.year, 2016)
item_id = item.id
self.load_plugins('zero')
with control_stdin('y'):
self.run_command('zero')
item = self.lib.get_item(item_id)
self.assertEqual(item['year'], 2016)
self.assertEqual(mediafile.year, 2016)
def test_whitelist_and_blacklist(self):
item = self.add_item_fixture(year=2016)
item.write()
mf = MediaFile(syspath(item.path))
self.assertEqual(mf.year, 2016)
item_id = item.id
self.config['zero']['fields'] = [u'year']
self.config['zero']['keep_fields'] = [u'comments']
self.load_plugins('zero')
with control_stdin('y'):
self.run_command('zero')
item = self.lib.get_item(item_id)
self.assertEqual(item['year'], 2016)
self.assertEqual(mf.year, 2016)
def test_keep_fields(self):
item = self.add_item_fixture(year=2016, comments=u'test comment')
self.config['zero']['keep_fields'] = [u'year']
self.config['zero']['fields'] = None
self.config['zero']['update_database'] = True
tags = {
'comments': u'test comment',
'year': 2016,
}
self.load_plugins('zero')
z = ZeroPlugin()
z.write_event(item, item.path, tags)
self.assertEqual(tags['comments'], None)
self.assertEqual(tags['year'], 2016)
def test_keep_fields_removes_preserved_tags(self):
self.config['zero']['keep_fields'] = [u'year']
self.config['zero']['fields'] = None
self.config['zero']['update_database'] = True
z = ZeroPlugin()
self.assertNotIn('id', z.fields_to_progs)
def test_fields_removes_preserved_tags(self):
self.config['zero']['fields'] = [u'year id']
self.config['zero']['update_database'] = True
z = ZeroPlugin()
self.assertNotIn('id', z.fields_to_progs)
def test_empty_query_n_response_no_changes(self):
item = self.add_item_fixture(
year=2016,
day=13,
month=3,
comments=u'test comment'
)
item.write()
item_id = item.id
self.config['zero']['fields'] = ['comments']
self.config['zero']['update_database'] = True
self.config['zero']['auto'] = False
self.load_plugins('zero')
with control_stdin('n'):
self.run_command('zero')
mf = MediaFile(syspath(item.path))
item = self.lib.get_item(item_id)
self.assertEqual(item['year'], 2016)
self.assertEqual(mf.year, 2016)
self.assertEqual(mf.comments, u'test comment')
self.assertEqual(item['comments'], u'test comment')
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/test_play.py 0000644 0000765 0000024 00000011521 13120341455 017123 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Jesse Weinstein
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Tests for the play plugin"""
from __future__ import division, absolute_import, print_function
import os
import unittest
from mock import patch, ANY
from test.helper import TestHelper, control_stdin
from beets.ui import UserError
from beets.util import open_anything
@patch('beetsplug.play.util.interactive_open')
class PlayPluginTest(unittest.TestCase, TestHelper):
def setUp(self):
self.setup_beets()
self.load_plugins('play')
self.item = self.add_item(album=u'a nice älbum', title=u'aNiceTitle')
self.lib.add_album([self.item])
self.config['play']['command'] = 'echo'
def tearDown(self):
self.teardown_beets()
self.unload_plugins()
def run_and_assert(self, open_mock, args=('title:aNiceTitle',),
expected_cmd='echo', expected_playlist=None):
self.run_command('play', *args)
open_mock.assert_called_once_with(ANY, expected_cmd)
expected_playlist = expected_playlist or self.item.path.decode('utf-8')
exp_playlist = expected_playlist + u'\n'
with open(open_mock.call_args[0][0][0], 'rb') as playlist:
self.assertEqual(exp_playlist, playlist.read().decode('utf-8'))
def test_basic(self, open_mock):
self.run_and_assert(open_mock)
def test_album_option(self, open_mock):
self.run_and_assert(open_mock, [u'-a', u'nice'])
def test_args_option(self, open_mock):
self.run_and_assert(
open_mock, [u'-A', u'foo', u'title:aNiceTitle'], u'echo foo')
def test_args_option_in_middle(self, open_mock):
self.config['play']['command'] = 'echo $args other'
self.run_and_assert(
open_mock, [u'-A', u'foo', u'title:aNiceTitle'], u'echo foo other')
def test_unset_args_option_in_middle(self, open_mock):
self.config['play']['command'] = 'echo $args other'
self.run_and_assert(
open_mock, [u'title:aNiceTitle'], u'echo other')
def test_relative_to(self, open_mock):
self.config['play']['command'] = 'echo'
self.config['play']['relative_to'] = '/something'
path = os.path.relpath(self.item.path, b'/something')
playlist = path.decode('utf-8')
self.run_and_assert(
open_mock, expected_cmd='echo', expected_playlist=playlist)
def test_use_folders(self, open_mock):
self.config['play']['command'] = None
self.config['play']['use_folders'] = True
self.run_command('play', '-a', 'nice')
open_mock.assert_called_once_with(ANY, open_anything())
with open(open_mock.call_args[0][0][0], 'rb') as f:
playlist = f.read().decode('utf-8')
self.assertEqual(u'{}\n'.format(
os.path.dirname(self.item.path.decode('utf-8'))),
playlist)
def test_raw(self, open_mock):
self.config['play']['raw'] = True
self.run_command(u'play', u'nice')
open_mock.assert_called_once_with([self.item.path], 'echo')
def test_not_found(self, open_mock):
self.run_command(u'play', u'not found')
open_mock.assert_not_called()
def test_warning_threshold(self, open_mock):
self.config['play']['warning_threshold'] = 1
self.add_item(title='another NiceTitle')
with control_stdin("a"):
self.run_command(u'play', u'nice')
open_mock.assert_not_called()
def test_skip_warning_threshold_bypass(self, open_mock):
self.config['play']['warning_threshold'] = 1
self.other_item = self.add_item(title='another NiceTitle')
expected_playlist = u'{0}\n{1}'.format(
self.item.path.decode('utf-8'),
self.other_item.path.decode('utf-8'))
with control_stdin("a"):
self.run_and_assert(
open_mock,
[u'-y', u'NiceTitle'],
expected_playlist=expected_playlist)
def test_command_failed(self, open_mock):
open_mock.side_effect = OSError(u"some reason")
with self.assertRaises(UserError):
self.run_command(u'play', u'title:aNiceTitle')
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/test_hook.py 0000644 0000765 0000024 00000007034 13025125203 017115 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2015, Thomas Scholtes.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
from __future__ import division, absolute_import, print_function
import os.path
import tempfile
import unittest
from test import _common
from test.helper import TestHelper
from beets import config
from beets import plugins
def get_temporary_path():
temporary_directory = tempfile._get_default_tempdir()
temporary_name = next(tempfile._get_candidate_names())
return os.path.join(temporary_directory, temporary_name)
class HookTest(_common.TestCase, TestHelper):
TEST_HOOK_COUNT = 5
def setUp(self):
self.setup_beets() # Converter is threaded
def tearDown(self):
self.unload_plugins()
self.teardown_beets()
def _add_hook(self, event, command):
hook = {
'event': event,
'command': command
}
hooks = config['hook']['hooks'].get(list) if 'hook' in config else []
hooks.append(hook)
config['hook']['hooks'] = hooks
def test_hook_no_arguments(self):
temporary_paths = [
get_temporary_path() for i in range(self.TEST_HOOK_COUNT)
]
for index, path in enumerate(temporary_paths):
self._add_hook('test_no_argument_event_{0}'.format(index),
'touch "{0}"'.format(path))
self.load_plugins('hook')
for index in range(len(temporary_paths)):
plugins.send('test_no_argument_event_{0}'.format(index))
for path in temporary_paths:
self.assertTrue(os.path.isfile(path))
os.remove(path)
def test_hook_event_substitution(self):
temporary_directory = tempfile._get_default_tempdir()
event_names = ['test_event_event_{0}'.format(i) for i in
range(self.TEST_HOOK_COUNT)]
for event in event_names:
self._add_hook(event,
'touch "{0}/{{event}}"'.format(temporary_directory))
self.load_plugins('hook')
for event in event_names:
plugins.send(event)
for event in event_names:
path = os.path.join(temporary_directory, event)
self.assertTrue(os.path.isfile(path))
os.remove(path)
def test_hook_argument_substitution(self):
temporary_paths = [
get_temporary_path() for i in range(self.TEST_HOOK_COUNT)
]
for index, path in enumerate(temporary_paths):
self._add_hook('test_argument_event_{0}'.format(index),
'touch "{path}"')
self.load_plugins('hook')
for index, path in enumerate(temporary_paths):
plugins.send('test_argument_event_{0}'.format(index), path=path)
for path in temporary_paths:
self.assertTrue(os.path.isfile(path))
os.remove(path)
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/test_edit.py 0000644 0000765 0000024 00000050140 13150552332 017104 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Adrian Sampson and Diego Moreda.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
from __future__ import division, absolute_import, print_function
import codecs
import unittest
from mock import patch
from test import _common
from test.helper import TestHelper, control_stdin
from test.test_ui_importer import TerminalImportSessionSetup
from test.test_importer import ImportHelper, AutotagStub
from beets.dbcore.query import TrueQuery
from beets.library import Item
from beetsplug.edit import EditPlugin
class ModifyFileMocker(object):
"""Helper for modifying a file, replacing or editing its contents. Used for
mocking the calls to the external editor during testing.
"""
def __init__(self, contents=None, replacements=None):
""" `self.contents` and `self.replacements` are initialized here, in
order to keep the rest of the functions of this class with the same
signature as `EditPlugin.get_editor()`, making mocking easier.
- `contents`: string with the contents of the file to be used for
`overwrite_contents()`
- `replacement`: dict with the in-place replacements to be used for
`replace_contents()`, in the form {'previous string': 'new string'}
TODO: check if it can be solved more elegantly with a decorator
"""
self.contents = contents
self.replacements = replacements
self.action = self.overwrite_contents
if replacements:
self.action = self.replace_contents
# The two methods below mock the `edit` utility function in the plugin.
def overwrite_contents(self, filename, log):
"""Modify `filename`, replacing its contents with `self.contents`. If
`self.contents` is empty, the file remains unchanged.
"""
if self.contents:
with codecs.open(filename, 'w', encoding='utf-8') as f:
f.write(self.contents)
def replace_contents(self, filename, log):
"""Modify `filename`, reading its contents and replacing the strings
specified in `self.replacements`.
"""
with codecs.open(filename, 'r', encoding='utf-8') as f:
contents = f.read()
for old, new_ in self.replacements.items():
contents = contents.replace(old, new_)
with codecs.open(filename, 'w', encoding='utf-8') as f:
f.write(contents)
class EditMixin(object):
"""Helper containing some common functionality used for the Edit tests."""
def assertItemFieldsModified(self, library_items, items, fields=[], # noqa
allowed=['path']):
"""Assert that items in the library (`lib_items`) have different values
on the specified `fields` (and *only* on those fields), compared to
`items`.
An empty `fields` list results in asserting that no modifications have
been performed. `allowed` is a list of field changes that are ignored
(they may or may not have changed; the assertion doesn't care).
"""
for lib_item, item in zip(library_items, items):
diff_fields = [field for field in lib_item._fields
if lib_item[field] != item[field]]
self.assertEqual(set(diff_fields).difference(allowed),
set(fields))
def run_mocked_interpreter(self, modify_file_args={}, stdin=[]):
"""Run the edit command during an import session, with mocked stdin and
yaml writing.
"""
m = ModifyFileMocker(**modify_file_args)
with patch('beetsplug.edit.edit', side_effect=m.action):
with control_stdin('\n'.join(stdin)):
self.importer.run()
def run_mocked_command(self, modify_file_args={}, stdin=[], args=[]):
"""Run the edit command, with mocked stdin and yaml writing, and
passing `args` to `run_command`."""
m = ModifyFileMocker(**modify_file_args)
with patch('beetsplug.edit.edit', side_effect=m.action):
with control_stdin('\n'.join(stdin)):
self.run_command('edit', *args)
@_common.slow_test()
@patch('beets.library.Item.write')
class EditCommandTest(unittest.TestCase, TestHelper, EditMixin):
"""Black box tests for `beetsplug.edit`. Command line interaction is
simulated using `test.helper.control_stdin()`, and yaml editing via an
external editor is simulated using `ModifyFileMocker`.
"""
ALBUM_COUNT = 1
TRACK_COUNT = 10
def setUp(self):
self.setup_beets()
self.load_plugins('edit')
# Add an album, storing the original fields for comparison.
self.album = self.add_album_fixture(track_count=self.TRACK_COUNT)
self.album_orig = {f: self.album[f] for f in self.album._fields}
self.items_orig = [{f: item[f] for f in item._fields} for
item in self.album.items()]
def tearDown(self):
EditPlugin.listeners = None
self.teardown_beets()
self.unload_plugins()
def assertCounts(self, mock_write, album_count=ALBUM_COUNT, track_count=TRACK_COUNT, # noqa
write_call_count=TRACK_COUNT, title_starts_with=''):
"""Several common assertions on Album, Track and call counts."""
self.assertEqual(len(self.lib.albums()), album_count)
self.assertEqual(len(self.lib.items()), track_count)
self.assertEqual(mock_write.call_count, write_call_count)
self.assertTrue(all(i.title.startswith(title_starts_with)
for i in self.lib.items()))
def test_title_edit_discard(self, mock_write):
"""Edit title for all items in the library, then discard changes."""
# Edit track titles.
self.run_mocked_command({'replacements': {u't\u00eftle':
u'modified t\u00eftle'}},
# Cancel.
['c'])
self.assertCounts(mock_write, write_call_count=0,
title_starts_with=u't\u00eftle')
self.assertItemFieldsModified(self.album.items(), self.items_orig, [])
def test_title_edit_apply(self, mock_write):
"""Edit title for all items in the library, then apply changes."""
# Edit track titles.
self.run_mocked_command({'replacements': {u't\u00eftle':
u'modified t\u00eftle'}},
# Apply changes.
['a'])
self.assertCounts(mock_write, write_call_count=self.TRACK_COUNT,
title_starts_with=u'modified t\u00eftle')
self.assertItemFieldsModified(self.album.items(), self.items_orig,
['title', 'mtime'])
def test_single_title_edit_apply(self, mock_write):
"""Edit title for one item in the library, then apply changes."""
# Edit one track title.
self.run_mocked_command({'replacements': {u't\u00eftle 9':
u'modified t\u00eftle 9'}},
# Apply changes.
['a'])
self.assertCounts(mock_write, write_call_count=1,)
# No changes except on last item.
self.assertItemFieldsModified(list(self.album.items())[:-1],
self.items_orig[:-1], [])
self.assertEqual(list(self.album.items())[-1].title,
u'modified t\u00eftle 9')
def test_noedit(self, mock_write):
"""Do not edit anything."""
# Do not edit anything.
self.run_mocked_command({'contents': None},
# No stdin.
[])
self.assertCounts(mock_write, write_call_count=0,
title_starts_with=u't\u00eftle')
self.assertItemFieldsModified(self.album.items(), self.items_orig, [])
def test_album_edit_apply(self, mock_write):
"""Edit the album field for all items in the library, apply changes.
By design, the album should not be updated.""
"""
# Edit album.
self.run_mocked_command({'replacements': {u'\u00e4lbum':
u'modified \u00e4lbum'}},
# Apply changes.
['a'])
self.assertCounts(mock_write, write_call_count=self.TRACK_COUNT)
self.assertItemFieldsModified(self.album.items(), self.items_orig,
['album', 'mtime'])
# Ensure album is *not* modified.
self.album.load()
self.assertEqual(self.album.album, u'\u00e4lbum')
def test_single_edit_add_field(self, mock_write):
"""Edit the yaml file appending an extra field to the first item, then
apply changes."""
# Append "foo: bar" to item with id == 2. ("id: 1" would match both
# "id: 1" and "id: 10")
self.run_mocked_command({'replacements': {u"id: 2":
u"id: 2\nfoo: bar"}},
# Apply changes.
['a'])
self.assertEqual(self.lib.items(u'id:2')[0].foo, 'bar')
# Even though a flexible attribute was written (which is not directly
# written to the tags), write should still be called since templates
# might use it.
self.assertCounts(mock_write, write_call_count=1,
title_starts_with=u't\u00eftle')
def test_a_album_edit_apply(self, mock_write):
"""Album query (-a), edit album field, apply changes."""
self.run_mocked_command({'replacements': {u'\u00e4lbum':
u'modified \u00e4lbum'}},
# Apply changes.
['a'],
args=['-a'])
self.album.load()
self.assertCounts(mock_write, write_call_count=self.TRACK_COUNT)
self.assertEqual(self.album.album, u'modified \u00e4lbum')
self.assertItemFieldsModified(self.album.items(), self.items_orig,
['album', 'mtime'])
def test_a_albumartist_edit_apply(self, mock_write):
"""Album query (-a), edit albumartist field, apply changes."""
self.run_mocked_command({'replacements': {u'album artist':
u'modified album artist'}},
# Apply changes.
['a'],
args=['-a'])
self.album.load()
self.assertCounts(mock_write, write_call_count=self.TRACK_COUNT)
self.assertEqual(self.album.albumartist, u'the modified album artist')
self.assertItemFieldsModified(self.album.items(), self.items_orig,
['albumartist', 'mtime'])
def test_malformed_yaml(self, mock_write):
"""Edit the yaml file incorrectly (resulting in a malformed yaml
document)."""
# Edit the yaml file to an invalid file.
self.run_mocked_command({'contents': '!MALFORMED'},
# Edit again to fix? No.
['n'])
self.assertCounts(mock_write, write_call_count=0,
title_starts_with=u't\u00eftle')
def test_invalid_yaml(self, mock_write):
"""Edit the yaml file incorrectly (resulting in a well-formed but
invalid yaml document)."""
# Edit the yaml file to an invalid but parseable file.
self.run_mocked_command({'contents': u'wellformed: yes, but invalid'},
# No stdin.
[])
self.assertCounts(mock_write, write_call_count=0,
title_starts_with=u't\u00eftle')
@_common.slow_test()
class EditDuringImporterTest(TerminalImportSessionSetup, unittest.TestCase,
ImportHelper, TestHelper, EditMixin):
"""TODO
"""
IGNORED = ['added', 'album_id', 'id', 'mtime', 'path']
def setUp(self):
self.setup_beets()
self.load_plugins('edit')
# Create some mediafiles, and store them for comparison.
self._create_import_dir(3)
self.items_orig = [Item.from_path(f.path) for f in self.media_files]
self.matcher = AutotagStub().install()
self.matcher.matching = AutotagStub.GOOD
self.config['import']['timid'] = True
def tearDown(self):
EditPlugin.listeners = None
self.unload_plugins()
self.teardown_beets()
self.matcher.restore()
def test_edit_apply_asis(self):
"""Edit the album field for all items in the library, apply changes,
using the original item tags.
"""
self._setup_import_session()
# Edit track titles.
self.run_mocked_interpreter({'replacements': {u'Tag Title':
u'Edited Title'}},
# eDit, Apply changes.
['d', 'a'])
# Check that only the 'title' field is modified.
self.assertItemFieldsModified(self.lib.items(), self.items_orig,
['title'],
self.IGNORED + ['albumartist',
'mb_albumartistid'])
self.assertTrue(all('Edited Title' in i.title
for i in self.lib.items()))
# Ensure album is *not* fetched from a candidate.
self.assertEqual(self.lib.albums()[0].mb_albumid, u'')
def test_edit_discard_asis(self):
"""Edit the album field for all items in the library, discard changes,
using the original item tags.
"""
self._setup_import_session()
# Edit track titles.
self.run_mocked_interpreter({'replacements': {u'Tag Title':
u'Edited Title'}},
# eDit, Cancel, Use as-is.
['d', 'c', 'u'])
# Check that nothing is modified, the album is imported ASIS.
self.assertItemFieldsModified(self.lib.items(), self.items_orig,
[],
self.IGNORED + ['albumartist',
'mb_albumartistid'])
self.assertTrue(all('Tag Title' in i.title
for i in self.lib.items()))
# Ensure album is *not* fetched from a candidate.
self.assertEqual(self.lib.albums()[0].mb_albumid, u'')
def test_edit_apply_candidate(self):
"""Edit the album field for all items in the library, apply changes,
using a candidate.
"""
self._setup_import_session()
# Edit track titles.
self.run_mocked_interpreter({'replacements': {u'Applied Title':
u'Edited Title'}},
# edit Candidates, 1, Apply changes.
['c', '1', 'a'])
# Check that 'title' field is modified, and other fields come from
# the candidate.
self.assertTrue(all('Edited Title ' in i.title
for i in self.lib.items()))
self.assertTrue(all('match ' in i.mb_trackid
for i in self.lib.items()))
# Ensure album is fetched from a candidate.
self.assertIn('albumid', self.lib.albums()[0].mb_albumid)
def test_edit_retag_apply(self):
"""Import the album using a candidate, then retag and edit and apply
changes.
"""
self._setup_import_session()
self.run_mocked_interpreter({},
# 1, Apply changes.
['1', 'a'])
# Retag and edit track titles. On retag, the importer will reset items
# ids but not the db connections.
self.importer.paths = []
self.importer.query = TrueQuery()
self.run_mocked_interpreter({'replacements': {u'Applied Title':
u'Edited Title'}},
# eDit, Apply changes.
['d', 'a'])
# Check that 'title' field is modified, and other fields come from
# the candidate.
self.assertTrue(all('Edited Title ' in i.title
for i in self.lib.items()))
self.assertTrue(all('match ' in i.mb_trackid
for i in self.lib.items()))
# Ensure album is fetched from a candidate.
self.assertIn('albumid', self.lib.albums()[0].mb_albumid)
def test_edit_discard_candidate(self):
"""Edit the album field for all items in the library, discard changes,
using a candidate.
"""
self._setup_import_session()
# Edit track titles.
self.run_mocked_interpreter({'replacements': {u'Applied Title':
u'Edited Title'}},
# edit Candidates, 1, Apply changes.
['c', '1', 'a'])
# Check that 'title' field is modified, and other fields come from
# the candidate.
self.assertTrue(all('Edited Title ' in i.title
for i in self.lib.items()))
self.assertTrue(all('match ' in i.mb_trackid
for i in self.lib.items()))
# Ensure album is fetched from a candidate.
self.assertIn('albumid', self.lib.albums()[0].mb_albumid)
def test_edit_apply_asis_singleton(self):
"""Edit the album field for all items in the library, apply changes,
using the original item tags and singleton mode.
"""
self._setup_import_session(singletons=True)
# Edit track titles.
self.run_mocked_interpreter({'replacements': {u'Tag Title':
u'Edited Title'}},
# eDit, Apply changes, aBort.
['d', 'a', 'b'])
# Check that only the 'title' field is modified.
self.assertItemFieldsModified(self.lib.items(), self.items_orig,
['title'],
self.IGNORED + ['albumartist',
'mb_albumartistid'])
self.assertTrue(all('Edited Title' in i.title
for i in self.lib.items()))
def test_edit_apply_candidate_singleton(self):
"""Edit the album field for all items in the library, apply changes,
using a candidate and singleton mode.
"""
self._setup_import_session()
# Edit track titles.
self.run_mocked_interpreter({'replacements': {u'Applied Title':
u'Edited Title'}},
# edit Candidates, 1, Apply changes, aBort.
['c', '1', 'a', 'b'])
# Check that 'title' field is modified, and other fields come from
# the candidate.
self.assertTrue(all('Edited Title ' in i.title
for i in self.lib.items()))
self.assertTrue(all('match ' in i.mb_trackid
for i in self.lib.items()))
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/test_web.py 0000644 0000765 0000024 00000012715 13122255064 016743 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
"""Tests for the 'web' plugin"""
from __future__ import division, absolute_import, print_function
import json
import unittest
import os.path
from six import assertCountEqual
from test import _common
from beets.library import Item, Album
from beetsplug import web
class WebPluginTest(_common.LibTestCase):
def setUp(self):
super(WebPluginTest, self).setUp()
# Add fixtures
for track in self.lib.items():
track.remove()
self.lib.add(Item(title=u'title', path='/path_1', id=1))
self.lib.add(Item(title=u'another title', path='/path_2', id=2))
self.lib.add(Album(album=u'album', id=3))
self.lib.add(Album(album=u'another album', id=4))
web.app.config['TESTING'] = True
web.app.config['lib'] = self.lib
web.app.config['INCLUDE_PATHS'] = False
self.client = web.app.test_client()
def test_config_include_paths_true(self):
web.app.config['INCLUDE_PATHS'] = True
response = self.client.get('/item/1')
response.json = json.loads(response.data.decode('utf-8'))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json['path'], u'/path_1')
def test_config_include_paths_false(self):
web.app.config['INCLUDE_PATHS'] = False
response = self.client.get('/item/1')
response.json = json.loads(response.data.decode('utf-8'))
self.assertEqual(response.status_code, 200)
self.assertNotIn('path', response.json)
def test_get_all_items(self):
response = self.client.get('/item/')
response.json = json.loads(response.data.decode('utf-8'))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json['items']), 2)
def test_get_single_item_by_id(self):
response = self.client.get('/item/1')
response.json = json.loads(response.data.decode('utf-8'))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json['id'], 1)
self.assertEqual(response.json['title'], u'title')
def test_get_multiple_items_by_id(self):
response = self.client.get('/item/1,2')
response.json = json.loads(response.data.decode('utf-8'))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json['items']), 2)
response_titles = [item['title'] for item in response.json['items']]
assertCountEqual(self, response_titles, [u'title', u'another title'])
def test_get_single_item_not_found(self):
response = self.client.get('/item/3')
self.assertEqual(response.status_code, 404)
def test_get_single_item_by_path(self):
data_path = os.path.join(_common.RSRC, b'full.mp3')
self.lib.add(Item.from_path(data_path))
response = self.client.get('/item/path/' + data_path.decode('utf-8'))
response.json = json.loads(response.data.decode('utf-8'))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json['title'], u'full')
def test_get_single_item_by_path_not_found_if_not_in_library(self):
data_path = os.path.join(_common.RSRC, b'full.mp3')
# data_path points to a valid file, but we have not added the file
# to the library.
response = self.client.get('/item/path/' + data_path.decode('utf-8'))
self.assertEqual(response.status_code, 404)
def test_get_item_empty_query(self):
response = self.client.get('/item/query/')
response.json = json.loads(response.data.decode('utf-8'))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json['items']), 2)
def test_get_simple_item_query(self):
response = self.client.get('/item/query/another')
response.json = json.loads(response.data.decode('utf-8'))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json['results']), 1)
self.assertEqual(response.json['results'][0]['title'],
u'another title')
def test_get_all_albums(self):
response = self.client.get('/album/')
response.json = json.loads(response.data.decode('utf-8'))
self.assertEqual(response.status_code, 200)
response_albums = [album['album'] for album in response.json['albums']]
assertCountEqual(self, response_albums, [u'album', u'another album'])
def test_get_single_album_by_id(self):
response = self.client.get('/album/2')
response.json = json.loads(response.data.decode('utf-8'))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json['id'], 2)
self.assertEqual(response.json['album'], u'another album')
def test_get_multiple_albums_by_id(self):
response = self.client.get('/album/1,2')
response.json = json.loads(response.data.decode('utf-8'))
self.assertEqual(response.status_code, 200)
response_albums = [album['album'] for album in response.json['albums']]
assertCountEqual(self, response_albums, [u'album', u'another album'])
def test_get_album_empty_query(self):
response = self.client.get('/album/query/')
response.json = json.loads(response.data.decode('utf-8'))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json['albums']), 2)
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/_common.py 0000644 0000765 0000024 00000024602 13175434666 016573 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Some common functionality for beets' test cases."""
from __future__ import division, absolute_import, print_function
import time
import sys
import os
import tempfile
import shutil
import six
import unittest
from contextlib import contextmanager
# Mangle the search path to include the beets sources.
sys.path.insert(0, '..')
import beets.library # noqa: E402
from beets import importer, logging # noqa: E402
from beets.ui import commands # noqa: E402
from beets import util # noqa: E402
import beets # noqa: E402
# Make sure the development versions of the plugins are used
import beetsplug # noqa: E402
beetsplug.__path__ = [os.path.abspath(
os.path.join(__file__, '..', '..', 'beetsplug')
)]
# Test resources path.
RSRC = util.bytestring_path(os.path.join(os.path.dirname(__file__), 'rsrc'))
PLUGINPATH = os.path.join(os.path.dirname(__file__), 'rsrc', 'beetsplug')
# Propagate to root logger so nosetest can capture it
log = logging.getLogger('beets')
log.propagate = True
log.setLevel(logging.DEBUG)
# Dummy item creation.
_item_ident = 0
# OS feature test.
HAVE_SYMLINK = sys.platform != 'win32'
HAVE_HARDLINK = sys.platform != 'win32'
def item(lib=None):
global _item_ident
_item_ident += 1
i = beets.library.Item(
title=u'the title',
artist=u'the artist',
albumartist=u'the album artist',
album=u'the album',
genre=u'the genre',
lyricist=u'the lyricist',
composer=u'the composer',
arranger=u'the arranger',
grouping=u'the grouping',
year=1,
month=2,
day=3,
track=4,
tracktotal=5,
disc=6,
disctotal=7,
lyrics=u'the lyrics',
comments=u'the comments',
bpm=8,
comp=True,
path='somepath{0}'.format(_item_ident),
length=60.0,
bitrate=128000,
format='FLAC',
mb_trackid='someID-1',
mb_albumid='someID-2',
mb_artistid='someID-3',
mb_albumartistid='someID-4',
album_id=None,
mtime=12345,
)
if lib:
lib.add(i)
return i
_album_ident = 0
def album(lib=None):
global _item_ident
_item_ident += 1
i = beets.library.Album(
artpath=None,
albumartist=u'some album artist',
albumartist_sort=u'some sort album artist',
albumartist_credit=u'some album artist credit',
album=u'the album',
genre=u'the genre',
year=2014,
month=2,
day=5,
tracktotal=0,
disctotal=1,
comp=False,
mb_albumid='someID-1',
mb_albumartistid='someID-1'
)
if lib:
lib.add(i)
return i
# Dummy import session.
def import_session(lib=None, loghandler=None, paths=[], query=[], cli=False):
cls = commands.TerminalImportSession if cli else importer.ImportSession
return cls(lib, loghandler, paths, query)
class Assertions(object):
"""A mixin with additional unit test assertions."""
def assertExists(self, path): # noqa
self.assertTrue(os.path.exists(util.syspath(path)),
u'file does not exist: {!r}'.format(path))
def assertNotExists(self, path): # noqa
self.assertFalse(os.path.exists(util.syspath(path)),
u'file exists: {!r}'.format((path)))
def assert_equal_path(self, a, b):
"""Check that two paths are equal."""
self.assertEqual(util.normpath(a), util.normpath(b),
u'paths are not equal: {!r} and {!r}'.format(a, b))
# A test harness for all beets tests.
# Provides temporary, isolated configuration.
class TestCase(unittest.TestCase, Assertions):
"""A unittest.TestCase subclass that saves and restores beets'
global configuration. This allows tests to make temporary
modifications that will then be automatically removed when the test
completes. Also provides some additional assertion methods, a
temporary directory, and a DummyIO.
"""
def setUp(self):
# A "clean" source list including only the defaults.
beets.config.sources = []
beets.config.read(user=False, defaults=True)
# Direct paths to a temporary directory. Tests can also use this
# temporary directory.
self.temp_dir = util.bytestring_path(tempfile.mkdtemp())
beets.config['statefile'] = \
util.py3_path(os.path.join(self.temp_dir, b'state.pickle'))
beets.config['library'] = \
util.py3_path(os.path.join(self.temp_dir, b'library.db'))
beets.config['directory'] = \
util.py3_path(os.path.join(self.temp_dir, b'libdir'))
# Set $HOME, which is used by confit's `config_dir()` to create
# directories.
self._old_home = os.environ.get('HOME')
os.environ['HOME'] = util.py3_path(self.temp_dir)
# Initialize, but don't install, a DummyIO.
self.io = DummyIO()
def tearDown(self):
if os.path.isdir(self.temp_dir):
shutil.rmtree(self.temp_dir)
if self._old_home is None:
del os.environ['HOME']
else:
os.environ['HOME'] = self._old_home
self.io.restore()
beets.config.clear()
beets.config._materialized = False
class LibTestCase(TestCase):
"""A test case that includes an in-memory library object (`lib`) and
an item added to the library (`i`).
"""
def setUp(self):
super(LibTestCase, self).setUp()
self.lib = beets.library.Library(':memory:')
self.i = item(self.lib)
def tearDown(self):
self.lib._connection().close()
super(LibTestCase, self).tearDown()
# Mock timing.
class Timecop(object):
"""Mocks the timing system (namely time() and sleep()) for testing.
Inspired by the Ruby timecop library.
"""
def __init__(self):
self.now = time.time()
def time(self):
return self.now
def sleep(self, amount):
self.now += amount
def install(self):
self.orig = {
'time': time.time,
'sleep': time.sleep,
}
time.time = self.time
time.sleep = self.sleep
def restore(self):
time.time = self.orig['time']
time.sleep = self.orig['sleep']
# Mock I/O.
class InputException(Exception):
def __init__(self, output=None):
self.output = output
def __str__(self):
msg = "Attempt to read with no input provided."
if self.output is not None:
msg += " Output: {!r}".format(self.output)
return msg
class DummyOut(object):
encoding = 'utf-8'
def __init__(self):
self.buf = []
def write(self, s):
self.buf.append(s)
def get(self):
if six.PY2:
return b''.join(self.buf)
else:
return ''.join(self.buf)
def flush(self):
self.clear()
def clear(self):
self.buf = []
class DummyIn(object):
encoding = 'utf-8'
def __init__(self, out=None):
self.buf = []
self.reads = 0
self.out = out
def add(self, s):
if six.PY2:
self.buf.append(s + b'\n')
else:
self.buf.append(s + '\n')
def readline(self):
if not self.buf:
if self.out:
raise InputException(self.out.get())
else:
raise InputException()
self.reads += 1
return self.buf.pop(0)
class DummyIO(object):
"""Mocks input and output streams for testing UI code."""
def __init__(self):
self.stdout = DummyOut()
self.stdin = DummyIn(self.stdout)
def addinput(self, s):
self.stdin.add(s)
def getoutput(self):
res = self.stdout.get()
self.stdout.clear()
return res
def readcount(self):
return self.stdin.reads
def install(self):
sys.stdin = self.stdin
sys.stdout = self.stdout
def restore(self):
sys.stdin = sys.__stdin__
sys.stdout = sys.__stdout__
# Utility.
def touch(path):
open(path, 'a').close()
class Bag(object):
"""An object that exposes a set of fields given as keyword
arguments. Any field not found in the dictionary appears to be None.
Used for mocking Album objects and the like.
"""
def __init__(self, **fields):
self.fields = fields
def __getattr__(self, key):
return self.fields.get(key)
# Convenience methods for setting up a temporary sandbox directory for tests
# that need to interact with the filesystem.
class TempDirMixin(object):
"""Text mixin for creating and deleting a temporary directory.
"""
def create_temp_dir(self):
"""Create a temporary directory and assign it into `self.temp_dir`.
Call `remove_temp_dir` later to delete it.
"""
path = tempfile.mkdtemp()
if not isinstance(path, bytes):
path = path.encode('utf8')
self.temp_dir = path
def remove_temp_dir(self):
"""Delete the temporary directory created by `create_temp_dir`.
"""
if os.path.isdir(self.temp_dir):
shutil.rmtree(self.temp_dir)
# Platform mocking.
@contextmanager
def platform_windows():
import ntpath
old_path = os.path
try:
os.path = ntpath
yield
finally:
os.path = old_path
@contextmanager
def platform_posix():
import posixpath
old_path = os.path
try:
os.path = posixpath
yield
finally:
os.path = old_path
@contextmanager
def system_mock(name):
import platform
old_system = platform.system
platform.system = lambda: name
try:
yield
finally:
platform.system = old_system
def slow_test(unused=None):
def _id(obj):
return obj
if 'SKIP_SLOW_TESTS' in os.environ:
return unittest.skip(u'test is slow')
return _id
beets-1.4.6/test/test_importer.py 0000644 0000765 0000024 00000210145 13215275541 020031 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
from __future__ import division, absolute_import, print_function
"""Tests for the general importer functionality.
"""
import os
import re
import shutil
import unicodedata
import sys
import stat
from six import StringIO
from tempfile import mkstemp
from zipfile import ZipFile
from tarfile import TarFile
from mock import patch, Mock
import unittest
from test import _common
from beets.util import displayable_path, bytestring_path, py3_path
from test.helper import TestImportSession, TestHelper, has_program, capture_log
from beets import importer
from beets.importer import albums_in_dir
from beets.mediafile import MediaFile
from beets import autotag
from beets.autotag import AlbumInfo, TrackInfo, AlbumMatch
from beets import config
from beets import logging
from beets import util
class AutotagStub(object):
"""Stub out MusicBrainz album and track matcher and control what the
autotagger returns.
"""
NONE = 'NONE'
IDENT = 'IDENT'
GOOD = 'GOOD'
BAD = 'BAD'
MISSING = 'MISSING'
"""Generate an album match for all but one track
"""
length = 2
matching = IDENT
def install(self):
self.mb_match_album = autotag.mb.match_album
self.mb_match_track = autotag.mb.match_track
self.mb_album_for_id = autotag.mb.album_for_id
self.mb_track_for_id = autotag.mb.track_for_id
autotag.mb.match_album = self.match_album
autotag.mb.match_track = self.match_track
autotag.mb.album_for_id = self.album_for_id
autotag.mb.track_for_id = self.track_for_id
return self
def restore(self):
autotag.mb.match_album = self.mb_match_album
autotag.mb.match_track = self.mb_match_track
autotag.mb.album_for_id = self.mb_album_for_id
autotag.mb.track_for_id = self.mb_track_for_id
def match_album(self, albumartist, album, tracks):
if self.matching == self.IDENT:
yield self._make_album_match(albumartist, album, tracks)
elif self.matching == self.GOOD:
for i in range(self.length):
yield self._make_album_match(albumartist, album, tracks, i)
elif self.matching == self.BAD:
for i in range(self.length):
yield self._make_album_match(albumartist, album, tracks, i + 1)
elif self.matching == self.MISSING:
yield self._make_album_match(albumartist, album, tracks, missing=1)
def match_track(self, artist, title):
yield TrackInfo(
title=title.replace('Tag', 'Applied'),
track_id=u'trackid',
artist=artist.replace('Tag', 'Applied'),
artist_id=u'artistid',
length=1,
index=0,
)
def album_for_id(self, mbid):
return None
def track_for_id(self, mbid):
return None
def _make_track_match(self, artist, album, number):
return TrackInfo(
title=u'Applied Title %d' % number,
track_id=u'match %d' % number,
artist=artist,
length=1,
index=0,
)
def _make_album_match(self, artist, album, tracks, distance=0, missing=0):
if distance:
id = ' ' + 'M' * distance
else:
id = ''
if artist is None:
artist = u"Various Artists"
else:
artist = artist.replace('Tag', 'Applied') + id
album = album.replace('Tag', 'Applied') + id
track_infos = []
for i in range(tracks - missing):
track_infos.append(self._make_track_match(artist, album, i + 1))
return AlbumInfo(
artist=artist,
album=album,
tracks=track_infos,
va=False,
album_id=u'albumid' + id,
artist_id=u'artistid' + id,
albumtype=u'soundtrack'
)
class ImportHelper(TestHelper):
"""Provides tools to setup a library, a directory containing files that are
to be imported and an import session. The class also provides stubs for the
autotagging library and several assertions for the library.
"""
def setup_beets(self, disk=False):
super(ImportHelper, self).setup_beets(disk)
self.lib.path_formats = [
(u'default', os.path.join('$artist', '$album', '$title')),
(u'singleton:true', os.path.join('singletons', '$title')),
(u'comp:true', os.path.join('compilations', '$album', '$title')),
]
def _create_import_dir(self, count=3):
"""Creates a directory with media files to import.
Sets ``self.import_dir`` to the path of the directory. Also sets
``self.import_media`` to a list :class:`MediaFile` for all the files in
the directory.
The directory has following layout
the_album/
track_1.mp3
track_2.mp3
track_3.mp3
:param count: Number of files to create
"""
self.import_dir = os.path.join(self.temp_dir, b'testsrcdir')
if os.path.isdir(self.import_dir):
shutil.rmtree(self.import_dir)
album_path = os.path.join(self.import_dir, b'the_album')
os.makedirs(album_path)
resource_path = os.path.join(_common.RSRC, b'full.mp3')
metadata = {
'artist': u'Tag Artist',
'album': u'Tag Album',
'albumartist': None,
'mb_trackid': None,
'mb_albumid': None,
'comp': None
}
self.media_files = []
for i in range(count):
# Copy files
medium_path = os.path.join(
album_path,
bytestring_path('track_%d.mp3' % (i + 1))
)
shutil.copy(resource_path, medium_path)
medium = MediaFile(medium_path)
# Set metadata
metadata['track'] = i + 1
metadata['title'] = u'Tag Title %d' % (i + 1)
for attr in metadata:
setattr(medium, attr, metadata[attr])
medium.save()
self.media_files.append(medium)
self.import_media = self.media_files
def _setup_import_session(self, import_dir=None, delete=False,
threaded=False, copy=True, singletons=False,
move=False, autotag=True, link=False,
hardlink=False):
config['import']['copy'] = copy
config['import']['delete'] = delete
config['import']['timid'] = True
config['threaded'] = False
config['import']['singletons'] = singletons
config['import']['move'] = move
config['import']['autotag'] = autotag
config['import']['resume'] = False
config['import']['link'] = link
config['import']['hardlink'] = hardlink
self.importer = TestImportSession(
self.lib, loghandler=None, query=None,
paths=[import_dir or self.import_dir]
)
def assert_file_in_lib(self, *segments):
"""Join the ``segments`` and assert that this path exists in the library
directory
"""
self.assertExists(os.path.join(self.libdir, *segments))
def assert_file_not_in_lib(self, *segments):
"""Join the ``segments`` and assert that this path exists in the library
directory
"""
self.assertNotExists(os.path.join(self.libdir, *segments))
def assert_lib_dir_empty(self):
self.assertEqual(len(os.listdir(self.libdir)), 0)
@_common.slow_test()
class NonAutotaggedImportTest(_common.TestCase, ImportHelper):
def setUp(self):
self.setup_beets(disk=True)
self._create_import_dir(2)
self._setup_import_session(autotag=False)
def tearDown(self):
self.teardown_beets()
def test_album_created_with_track_artist(self):
self.importer.run()
albums = self.lib.albums()
self.assertEqual(len(albums), 1)
self.assertEqual(albums[0].albumartist, u'Tag Artist')
def test_import_copy_arrives(self):
self.importer.run()
for mediafile in self.import_media:
self.assert_file_in_lib(
b'Tag Artist', b'Tag Album',
util.bytestring_path('{0}.mp3'.format(mediafile.title)))
def test_threaded_import_copy_arrives(self):
config['threaded'] = True
self.importer.run()
for mediafile in self.import_media:
self.assert_file_in_lib(
b'Tag Artist', b'Tag Album',
util.bytestring_path('{0}.mp3'.format(mediafile.title)))
def test_import_with_move_deletes_import_files(self):
config['import']['move'] = True
for mediafile in self.import_media:
self.assertExists(mediafile.path)
self.importer.run()
for mediafile in self.import_media:
self.assertNotExists(mediafile.path)
def test_import_with_move_prunes_directory_empty(self):
config['import']['move'] = True
self.assertExists(os.path.join(self.import_dir, b'the_album'))
self.importer.run()
self.assertNotExists(os.path.join(self.import_dir, b'the_album'))
def test_import_with_move_prunes_with_extra_clutter(self):
f = open(os.path.join(self.import_dir, b'the_album', b'alog.log'), 'w')
f.close()
config['clutter'] = ['*.log']
config['import']['move'] = True
self.assertExists(os.path.join(self.import_dir, b'the_album'))
self.importer.run()
self.assertNotExists(os.path.join(self.import_dir, b'the_album'))
def test_threaded_import_move_arrives(self):
config['import']['move'] = True
config['import']['threaded'] = True
self.importer.run()
for mediafile in self.import_media:
self.assert_file_in_lib(
b'Tag Artist', b'Tag Album',
util.bytestring_path('{0}.mp3'.format(mediafile.title)))
def test_threaded_import_move_deletes_import(self):
config['import']['move'] = True
config['threaded'] = True
self.importer.run()
for mediafile in self.import_media:
self.assertNotExists(mediafile.path)
def test_import_without_delete_retains_files(self):
config['import']['delete'] = False
self.importer.run()
for mediafile in self.import_media:
self.assertExists(mediafile.path)
def test_import_with_delete_removes_files(self):
config['import']['delete'] = True
self.importer.run()
for mediafile in self.import_media:
self.assertNotExists(mediafile.path)
def test_import_with_delete_prunes_directory_empty(self):
config['import']['delete'] = True
self.assertExists(os.path.join(self.import_dir, b'the_album'))
self.importer.run()
self.assertNotExists(os.path.join(self.import_dir, b'the_album'))
@unittest.skipUnless(_common.HAVE_SYMLINK, "need symlinks")
def test_import_link_arrives(self):
config['import']['link'] = True
self.importer.run()
for mediafile in self.import_media:
filename = os.path.join(
self.libdir,
b'Tag Artist', b'Tag Album',
util.bytestring_path('{0}.mp3'.format(mediafile.title))
)
self.assertExists(filename)
self.assertTrue(os.path.islink(filename))
self.assert_equal_path(
util.bytestring_path(os.readlink(filename)),
mediafile.path
)
@unittest.skipUnless(_common.HAVE_HARDLINK, "need hardlinks")
def test_import_hardlink_arrives(self):
config['import']['hardlink'] = True
self.importer.run()
for mediafile in self.import_media:
filename = os.path.join(
self.libdir,
b'Tag Artist', b'Tag Album',
util.bytestring_path('{0}.mp3'.format(mediafile.title))
)
self.assertExists(filename)
s1 = os.stat(mediafile.path)
s2 = os.stat(filename)
self.assertTrue(
(s1[stat.ST_INO], s1[stat.ST_DEV]) ==
(s2[stat.ST_INO], s2[stat.ST_DEV])
)
def create_archive(session):
(handle, path) = mkstemp(dir=py3_path(session.temp_dir))
os.close(handle)
archive = ZipFile(py3_path(path), mode='w')
archive.write(os.path.join(_common.RSRC, b'full.mp3'),
'full.mp3')
archive.close()
path = bytestring_path(path)
return path
class RmTempTest(unittest.TestCase, ImportHelper, _common.Assertions):
"""Tests that temporarily extracted archives are properly removed
after usage.
"""
def setUp(self):
self.setup_beets()
self.want_resume = False
self.config['incremental'] = False
self._old_home = None
def tearDown(self):
self.teardown_beets()
def test_rm(self):
zip_path = create_archive(self)
archive_task = importer.ArchiveImportTask(zip_path)
archive_task.extract()
tmp_path = archive_task.toppath
self._setup_import_session(autotag=False, import_dir=tmp_path)
self.assertExists(tmp_path)
archive_task.finalize(self)
self.assertNotExists(tmp_path)
class ImportZipTest(unittest.TestCase, ImportHelper):
def setUp(self):
self.setup_beets()
def tearDown(self):
self.teardown_beets()
def test_import_zip(self):
zip_path = create_archive(self)
self.assertEqual(len(self.lib.items()), 0)
self.assertEqual(len(self.lib.albums()), 0)
self._setup_import_session(autotag=False, import_dir=zip_path)
self.importer.run()
self.assertEqual(len(self.lib.items()), 1)
self.assertEqual(len(self.lib.albums()), 1)
class ImportTarTest(ImportZipTest):
def create_archive(self):
(handle, path) = mkstemp(dir=self.temp_dir)
os.close(handle)
archive = TarFile(py3_path(path), mode='w')
archive.add(os.path.join(_common.RSRC, b'full.mp3'),
'full.mp3')
archive.close()
return path
@unittest.skipIf(not has_program('unrar'), u'unrar program not found')
class ImportRarTest(ImportZipTest):
def create_archive(self):
return os.path.join(_common.RSRC, b'archive.rar')
@unittest.skip('Implement me!')
class ImportPasswordRarTest(ImportZipTest):
def create_archive(self):
return os.path.join(_common.RSRC, b'password.rar')
class ImportSingletonTest(_common.TestCase, ImportHelper):
"""Test ``APPLY`` and ``ASIS`` choices for an import session with singletons
config set to True.
"""
def setUp(self):
self.setup_beets()
self._create_import_dir(1)
self._setup_import_session()
config['import']['singletons'] = True
self.matcher = AutotagStub().install()
def tearDown(self):
self.teardown_beets()
self.matcher.restore()
def test_apply_asis_adds_track(self):
self.assertEqual(self.lib.items().get(), None)
self.importer.add_choice(importer.action.ASIS)
self.importer.run()
self.assertEqual(self.lib.items().get().title, u'Tag Title 1')
def test_apply_asis_does_not_add_album(self):
self.assertEqual(self.lib.albums().get(), None)
self.importer.add_choice(importer.action.ASIS)
self.importer.run()
self.assertEqual(self.lib.albums().get(), None)
def test_apply_asis_adds_singleton_path(self):
self.assert_lib_dir_empty()
self.importer.add_choice(importer.action.ASIS)
self.importer.run()
self.assert_file_in_lib(b'singletons', b'Tag Title 1.mp3')
def test_apply_candidate_adds_track(self):
self.assertEqual(self.lib.items().get(), None)
self.importer.add_choice(importer.action.APPLY)
self.importer.run()
self.assertEqual(self.lib.items().get().title, u'Applied Title 1')
def test_apply_candidate_does_not_add_album(self):
self.importer.add_choice(importer.action.APPLY)
self.importer.run()
self.assertEqual(self.lib.albums().get(), None)
def test_apply_candidate_adds_singleton_path(self):
self.assert_lib_dir_empty()
self.importer.add_choice(importer.action.APPLY)
self.importer.run()
self.assert_file_in_lib(b'singletons', b'Applied Title 1.mp3')
def test_skip_does_not_add_first_track(self):
self.importer.add_choice(importer.action.SKIP)
self.importer.run()
self.assertEqual(self.lib.items().get(), None)
def test_skip_adds_other_tracks(self):
self._create_import_dir(2)
self.importer.add_choice(importer.action.SKIP)
self.importer.add_choice(importer.action.ASIS)
self.importer.run()
self.assertEqual(len(self.lib.items()), 1)
def test_import_single_files(self):
resource_path = os.path.join(_common.RSRC, b'empty.mp3')
single_path = os.path.join(self.import_dir, b'track_2.mp3')
shutil.copy(resource_path, single_path)
import_files = [
os.path.join(self.import_dir, b'the_album'),
single_path
]
self._setup_import_session(singletons=False)
self.importer.paths = import_files
self.importer.add_choice(importer.action.ASIS)
self.importer.add_choice(importer.action.ASIS)
self.importer.run()
self.assertEqual(len(self.lib.items()), 2)
self.assertEqual(len(self.lib.albums()), 2)
def test_set_fields(self):
genre = u"\U0001F3B7 Jazz"
collection = u"To Listen"
config['import']['set_fields'] = {
u'collection': collection,
u'genre': genre
}
# As-is item import.
self.assertEqual(self.lib.albums().get(), None)
self.importer.add_choice(importer.action.ASIS)
self.importer.run()
for item in self.lib.items():
item.load() # TODO: Not sure this is necessary.
self.assertEqual(item.genre, genre)
self.assertEqual(item.collection, collection)
# Remove item from library to test again with APPLY choice.
item.remove()
# Autotagged.
self.assertEqual(self.lib.albums().get(), None)
self.importer.clear_choices()
self.importer.add_choice(importer.action.APPLY)
self.importer.run()
for item in self.lib.items():
item.load()
self.assertEqual(item.genre, genre)
self.assertEqual(item.collection, collection)
class ImportTest(_common.TestCase, ImportHelper):
"""Test APPLY, ASIS and SKIP choices.
"""
def setUp(self):
self.setup_beets()
self._create_import_dir(1)
self._setup_import_session()
self.matcher = AutotagStub().install()
self.matcher.macthin = AutotagStub.GOOD
def tearDown(self):
self.teardown_beets()
self.matcher.restore()
def test_apply_asis_adds_album(self):
self.assertEqual(self.lib.albums().get(), None)
self.importer.add_choice(importer.action.ASIS)
self.importer.run()
self.assertEqual(self.lib.albums().get().album, u'Tag Album')
def test_apply_asis_adds_tracks(self):
self.assertEqual(self.lib.items().get(), None)
self.importer.add_choice(importer.action.ASIS)
self.importer.run()
self.assertEqual(self.lib.items().get().title, u'Tag Title 1')
def test_apply_asis_adds_album_path(self):
self.assert_lib_dir_empty()
self.importer.add_choice(importer.action.ASIS)
self.importer.run()
self.assert_file_in_lib(
b'Tag Artist', b'Tag Album', b'Tag Title 1.mp3')
def test_apply_candidate_adds_album(self):
self.assertEqual(self.lib.albums().get(), None)
self.importer.add_choice(importer.action.APPLY)
self.importer.run()
self.assertEqual(self.lib.albums().get().album, u'Applied Album')
def test_apply_candidate_adds_tracks(self):
self.assertEqual(self.lib.items().get(), None)
self.importer.add_choice(importer.action.APPLY)
self.importer.run()
self.assertEqual(self.lib.items().get().title, u'Applied Title 1')
def test_apply_candidate_adds_album_path(self):
self.assert_lib_dir_empty()
self.importer.add_choice(importer.action.APPLY)
self.importer.run()
self.assert_file_in_lib(
b'Applied Artist', b'Applied Album', b'Applied Title 1.mp3')
def test_apply_from_scratch_removes_other_metadata(self):
config['import']['from_scratch'] = True
for mediafile in self.import_media:
mediafile.genre = u'Tag Genre'
mediafile.save()
self.importer.add_choice(importer.action.APPLY)
self.importer.run()
self.assertEqual(self.lib.items().get().genre, u'')
def test_apply_with_move_deletes_import(self):
config['import']['move'] = True
import_file = os.path.join(
self.import_dir, b'the_album', b'track_1.mp3')
self.assertExists(import_file)
self.importer.add_choice(importer.action.APPLY)
self.importer.run()
self.assertNotExists(import_file)
def test_apply_with_delete_deletes_import(self):
config['import']['delete'] = True
import_file = os.path.join(self.import_dir,
b'the_album', b'track_1.mp3')
self.assertExists(import_file)
self.importer.add_choice(importer.action.APPLY)
self.importer.run()
self.assertNotExists(import_file)
def test_skip_does_not_add_track(self):
self.importer.add_choice(importer.action.SKIP)
self.importer.run()
self.assertEqual(self.lib.items().get(), None)
def test_skip_non_album_dirs(self):
self.assertTrue(os.path.isdir(
os.path.join(self.import_dir, b'the_album')))
self.touch(b'cruft', dir=self.import_dir)
self.importer.add_choice(importer.action.APPLY)
self.importer.run()
self.assertEqual(len(self.lib.albums()), 1)
def test_unmatched_tracks_not_added(self):
self._create_import_dir(2)
self.matcher.matching = self.matcher.MISSING
self.importer.add_choice(importer.action.APPLY)
self.importer.run()
self.assertEqual(len(self.lib.items()), 1)
def test_empty_directory_warning(self):
import_dir = os.path.join(self.temp_dir, b'empty')
self.touch(b'non-audio', dir=import_dir)
self._setup_import_session(import_dir=import_dir)
with capture_log() as logs:
self.importer.run()
import_dir = displayable_path(import_dir)
self.assertIn(u'No files imported from {0}'.format(import_dir), logs)
def test_empty_directory_singleton_warning(self):
import_dir = os.path.join(self.temp_dir, b'empty')
self.touch(b'non-audio', dir=import_dir)
self._setup_import_session(import_dir=import_dir, singletons=True)
with capture_log() as logs:
self.importer.run()
import_dir = displayable_path(import_dir)
self.assertIn(u'No files imported from {0}'.format(import_dir), logs)
def test_asis_no_data_source(self):
self.assertEqual(self.lib.items().get(), None)
self.importer.add_choice(importer.action.ASIS)
self.importer.run()
with self.assertRaises(AttributeError):
self.lib.items().get().data_source
def test_set_fields(self):
genre = u"\U0001F3B7 Jazz"
collection = u"To Listen"
config['import']['set_fields'] = {
u'collection': collection,
u'genre': genre
}
# As-is album import.
self.assertEqual(self.lib.albums().get(), None)
self.importer.add_choice(importer.action.ASIS)
self.importer.run()
for album in self.lib.albums():
album.load() # TODO: Not sure this is necessary.
self.assertEqual(album.genre, genre)
self.assertEqual(album.collection, collection)
# Remove album from library to test again with APPLY choice.
album.remove()
# Autotagged.
self.assertEqual(self.lib.albums().get(), None)
self.importer.clear_choices()
self.importer.add_choice(importer.action.APPLY)
self.importer.run()
for album in self.lib.albums():
album.load()
self.assertEqual(album.genre, genre)
self.assertEqual(album.collection, collection)
class ImportTracksTest(_common.TestCase, ImportHelper):
"""Test TRACKS and APPLY choice.
"""
def setUp(self):
self.setup_beets()
self._create_import_dir(1)
self._setup_import_session()
self.matcher = AutotagStub().install()
def tearDown(self):
self.teardown_beets()
self.matcher.restore()
def test_apply_tracks_adds_singleton_track(self):
self.assertEqual(self.lib.items().get(), None)
self.assertEqual(self.lib.albums().get(), None)
self.importer.add_choice(importer.action.TRACKS)
self.importer.add_choice(importer.action.APPLY)
self.importer.add_choice(importer.action.APPLY)
self.importer.run()
self.assertEqual(self.lib.items().get().title, u'Applied Title 1')
self.assertEqual(self.lib.albums().get(), None)
def test_apply_tracks_adds_singleton_path(self):
self.assert_lib_dir_empty()
self.importer.add_choice(importer.action.TRACKS)
self.importer.add_choice(importer.action.APPLY)
self.importer.add_choice(importer.action.APPLY)
self.importer.run()
self.assert_file_in_lib(b'singletons', b'Applied Title 1.mp3')
class ImportCompilationTest(_common.TestCase, ImportHelper):
"""Test ASIS import of a folder containing tracks with different artists.
"""
def setUp(self):
self.setup_beets()
self._create_import_dir(3)
self._setup_import_session()
self.matcher = AutotagStub().install()
def tearDown(self):
self.teardown_beets()
self.matcher.restore()
def test_asis_homogenous_sets_albumartist(self):
self.importer.add_choice(importer.action.ASIS)
self.importer.run()
self.assertEqual(self.lib.albums().get().albumartist, u'Tag Artist')
for item in self.lib.items():
self.assertEqual(item.albumartist, u'Tag Artist')
def test_asis_heterogenous_sets_various_albumartist(self):
self.import_media[0].artist = u'Other Artist'
self.import_media[0].save()
self.import_media[1].artist = u'Another Artist'
self.import_media[1].save()
self.importer.add_choice(importer.action.ASIS)
self.importer.run()
self.assertEqual(self.lib.albums().get().albumartist,
u'Various Artists')
for item in self.lib.items():
self.assertEqual(item.albumartist, u'Various Artists')
def test_asis_heterogenous_sets_sompilation(self):
self.import_media[0].artist = u'Other Artist'
self.import_media[0].save()
self.import_media[1].artist = u'Another Artist'
self.import_media[1].save()
self.importer.add_choice(importer.action.ASIS)
self.importer.run()
for item in self.lib.items():
self.assertTrue(item.comp)
def test_asis_sets_majority_albumartist(self):
self.import_media[0].artist = u'Other Artist'
self.import_media[0].save()
self.import_media[1].artist = u'Other Artist'
self.import_media[1].save()
self.importer.add_choice(importer.action.ASIS)
self.importer.run()
self.assertEqual(self.lib.albums().get().albumartist, u'Other Artist')
for item in self.lib.items():
self.assertEqual(item.albumartist, u'Other Artist')
def test_asis_albumartist_tag_sets_albumartist(self):
self.import_media[0].artist = u'Other Artist'
self.import_media[1].artist = u'Another Artist'
for mediafile in self.import_media:
mediafile.albumartist = u'Album Artist'
mediafile.mb_albumartistid = u'Album Artist ID'
mediafile.save()
self.importer.add_choice(importer.action.ASIS)
self.importer.run()
self.assertEqual(self.lib.albums().get().albumartist, u'Album Artist')
self.assertEqual(self.lib.albums().get().mb_albumartistid,
u'Album Artist ID')
for item in self.lib.items():
self.assertEqual(item.albumartist, u'Album Artist')
self.assertEqual(item.mb_albumartistid, u'Album Artist ID')
class ImportExistingTest(_common.TestCase, ImportHelper):
"""Test importing files that are already in the library directory.
"""
def setUp(self):
self.setup_beets()
self._create_import_dir(1)
self.matcher = AutotagStub().install()
self._setup_import_session()
self.setup_importer = self.importer
self.setup_importer.default_choice = importer.action.APPLY
self._setup_import_session(import_dir=self.libdir)
def tearDown(self):
self.teardown_beets()
self.matcher.restore()
def test_does_not_duplicate_item(self):
self.setup_importer.run()
self.assertEqual(len((self.lib.items())), 1)
self.importer.add_choice(importer.action.APPLY)
self.importer.run()
self.assertEqual(len((self.lib.items())), 1)
def test_does_not_duplicate_album(self):
self.setup_importer.run()
self.assertEqual(len((self.lib.albums())), 1)
self.importer.add_choice(importer.action.APPLY)
self.importer.run()
self.assertEqual(len((self.lib.albums())), 1)
def test_does_not_duplicate_singleton_track(self):
self.setup_importer.add_choice(importer.action.TRACKS)
self.setup_importer.add_choice(importer.action.APPLY)
self.setup_importer.run()
self.assertEqual(len((self.lib.items())), 1)
self.importer.add_choice(importer.action.TRACKS)
self.importer.add_choice(importer.action.APPLY)
self.importer.run()
self.assertEqual(len((self.lib.items())), 1)
def test_asis_updates_metadata(self):
self.setup_importer.run()
medium = MediaFile(self.lib.items().get().path)
medium.title = u'New Title'
medium.save()
self.importer.add_choice(importer.action.ASIS)
self.importer.run()
self.assertEqual(self.lib.items().get().title, u'New Title')
def test_asis_updated_moves_file(self):
self.setup_importer.run()
medium = MediaFile(self.lib.items().get().path)
medium.title = u'New Title'
medium.save()
old_path = os.path.join(b'Applied Artist', b'Applied Album',
b'Applied Title 1.mp3')
self.assert_file_in_lib(old_path)
self.importer.add_choice(importer.action.ASIS)
self.importer.run()
self.assert_file_in_lib(b'Applied Artist', b'Applied Album',
b'New Title.mp3')
self.assert_file_not_in_lib(old_path)
def test_asis_updated_without_copy_does_not_move_file(self):
self.setup_importer.run()
medium = MediaFile(self.lib.items().get().path)
medium.title = u'New Title'
medium.save()
old_path = os.path.join(b'Applied Artist', b'Applied Album',
b'Applied Title 1.mp3')
self.assert_file_in_lib(old_path)
config['import']['copy'] = False
self.importer.add_choice(importer.action.ASIS)
self.importer.run()
self.assert_file_not_in_lib(b'Applied Artist', b'Applied Album',
b'New Title.mp3')
self.assert_file_in_lib(old_path)
def test_outside_file_is_copied(self):
config['import']['copy'] = False
self.setup_importer.run()
self.assert_equal_path(self.lib.items().get().path,
self.import_media[0].path)
config['import']['copy'] = True
self._setup_import_session()
self.importer.add_choice(importer.action.APPLY)
self.importer.run()
new_path = os.path.join(b'Applied Artist', b'Applied Album',
b'Applied Title 1.mp3')
self.assert_file_in_lib(new_path)
self.assert_equal_path(self.lib.items().get().path,
os.path.join(self.libdir, new_path))
def test_outside_file_is_moved(self):
config['import']['copy'] = False
self.setup_importer.run()
self.assert_equal_path(self.lib.items().get().path,
self.import_media[0].path)
self._setup_import_session(move=True)
self.importer.add_choice(importer.action.APPLY)
self.importer.run()
self.assertNotExists(self.import_media[0].path)
class GroupAlbumsImportTest(_common.TestCase, ImportHelper):
def setUp(self):
self.setup_beets()
self._create_import_dir(3)
self.matcher = AutotagStub().install()
self.matcher.matching = AutotagStub.NONE
self._setup_import_session()
# Split tracks into two albums and use both as-is
self.importer.add_choice(importer.action.ALBUMS)
self.importer.add_choice(importer.action.ASIS)
self.importer.add_choice(importer.action.ASIS)
def tearDown(self):
self.teardown_beets()
self.matcher.restore()
def test_add_album_for_different_artist_and_different_album(self):
self.import_media[0].artist = u"Artist B"
self.import_media[0].album = u"Album B"
self.import_media[0].save()
self.importer.run()
albums = set([album.album for album in self.lib.albums()])
self.assertEqual(albums, set(['Album B', 'Tag Album']))
def test_add_album_for_different_artist_and_same_albumartist(self):
self.import_media[0].artist = u"Artist B"
self.import_media[0].albumartist = u"Album Artist"
self.import_media[0].save()
self.import_media[1].artist = u"Artist C"
self.import_media[1].albumartist = u"Album Artist"
self.import_media[1].save()
self.importer.run()
artists = set([album.albumartist for album in self.lib.albums()])
self.assertEqual(artists, set(['Album Artist', 'Tag Artist']))
def test_add_album_for_same_artist_and_different_album(self):
self.import_media[0].album = u"Album B"
self.import_media[0].save()
self.importer.run()
albums = set([album.album for album in self.lib.albums()])
self.assertEqual(albums, set(['Album B', 'Tag Album']))
def test_add_album_for_same_album_and_different_artist(self):
self.import_media[0].artist = u"Artist B"
self.import_media[0].save()
self.importer.run()
artists = set([album.albumartist for album in self.lib.albums()])
self.assertEqual(artists, set(['Artist B', 'Tag Artist']))
def test_incremental(self):
config['import']['incremental'] = True
self.import_media[0].album = u"Album B"
self.import_media[0].save()
self.importer.run()
albums = set([album.album for album in self.lib.albums()])
self.assertEqual(albums, set(['Album B', 'Tag Album']))
class GlobalGroupAlbumsImportTest(GroupAlbumsImportTest):
def setUp(self):
super(GlobalGroupAlbumsImportTest, self).setUp()
self.importer.clear_choices()
self.importer.default_choice = importer.action.ASIS
config['import']['group_albums'] = True
class ChooseCandidateTest(_common.TestCase, ImportHelper):
def setUp(self):
self.setup_beets()
self._create_import_dir(1)
self._setup_import_session()
self.matcher = AutotagStub().install()
self.matcher.matching = AutotagStub.BAD
def tearDown(self):
self.teardown_beets()
self.matcher.restore()
def test_choose_first_candidate(self):
self.importer.add_choice(1)
self.importer.run()
self.assertEqual(self.lib.albums().get().album, u'Applied Album M')
def test_choose_second_candidate(self):
self.importer.add_choice(2)
self.importer.run()
self.assertEqual(self.lib.albums().get().album, u'Applied Album MM')
class InferAlbumDataTest(_common.TestCase):
def setUp(self):
super(InferAlbumDataTest, self).setUp()
i1 = _common.item()
i2 = _common.item()
i3 = _common.item()
i1.title = u'first item'
i2.title = u'second item'
i3.title = u'third item'
i1.comp = i2.comp = i3.comp = False
i1.albumartist = i2.albumartist = i3.albumartist = ''
i1.mb_albumartistid = i2.mb_albumartistid = i3.mb_albumartistid = ''
self.items = [i1, i2, i3]
self.task = importer.ImportTask(paths=['a path'], toppath='top path',
items=self.items)
def test_asis_homogenous_single_artist(self):
self.task.set_choice(importer.action.ASIS)
self.task.align_album_level_fields()
self.assertFalse(self.items[0].comp)
self.assertEqual(self.items[0].albumartist, self.items[2].artist)
def test_asis_heterogenous_va(self):
self.items[0].artist = u'another artist'
self.items[1].artist = u'some other artist'
self.task.set_choice(importer.action.ASIS)
self.task.align_album_level_fields()
self.assertTrue(self.items[0].comp)
self.assertEqual(self.items[0].albumartist, u'Various Artists')
def test_asis_comp_applied_to_all_items(self):
self.items[0].artist = u'another artist'
self.items[1].artist = u'some other artist'
self.task.set_choice(importer.action.ASIS)
self.task.align_album_level_fields()
for item in self.items:
self.assertTrue(item.comp)
self.assertEqual(item.albumartist, u'Various Artists')
def test_asis_majority_artist_single_artist(self):
self.items[0].artist = u'another artist'
self.task.set_choice(importer.action.ASIS)
self.task.align_album_level_fields()
self.assertFalse(self.items[0].comp)
self.assertEqual(self.items[0].albumartist, self.items[2].artist)
def test_asis_track_albumartist_override(self):
self.items[0].artist = u'another artist'
self.items[1].artist = u'some other artist'
for item in self.items:
item.albumartist = u'some album artist'
item.mb_albumartistid = u'some album artist id'
self.task.set_choice(importer.action.ASIS)
self.task.align_album_level_fields()
self.assertEqual(self.items[0].albumartist,
u'some album artist')
self.assertEqual(self.items[0].mb_albumartistid,
u'some album artist id')
def test_apply_gets_artist_and_id(self):
self.task.set_choice(AlbumMatch(0, None, {}, set(), set())) # APPLY
self.task.align_album_level_fields()
self.assertEqual(self.items[0].albumartist, self.items[0].artist)
self.assertEqual(self.items[0].mb_albumartistid,
self.items[0].mb_artistid)
def test_apply_lets_album_values_override(self):
for item in self.items:
item.albumartist = u'some album artist'
item.mb_albumartistid = u'some album artist id'
self.task.set_choice(AlbumMatch(0, None, {}, set(), set())) # APPLY
self.task.align_album_level_fields()
self.assertEqual(self.items[0].albumartist,
u'some album artist')
self.assertEqual(self.items[0].mb_albumartistid,
u'some album artist id')
def test_small_single_artist_album(self):
self.items = [self.items[0]]
self.task.items = self.items
self.task.set_choice(importer.action.ASIS)
self.task.align_album_level_fields()
self.assertFalse(self.items[0].comp)
def test_album_info(*args, **kwargs):
"""Create an AlbumInfo object for testing.
"""
track_info = TrackInfo(
title=u'new title',
track_id=u'trackid',
index=0,
)
album_info = AlbumInfo(
artist=u'artist',
album=u'album',
tracks=[track_info],
album_id=u'albumid',
artist_id=u'artistid',
)
return iter([album_info])
@patch('beets.autotag.mb.match_album', Mock(side_effect=test_album_info))
class ImportDuplicateAlbumTest(unittest.TestCase, TestHelper,
_common.Assertions):
def setUp(self):
self.setup_beets()
# Original album
self.add_album_fixture(albumartist=u'artist', album=u'album')
# Create import session
self.importer = self.create_importer()
config['import']['autotag'] = True
def tearDown(self):
self.teardown_beets()
def test_remove_duplicate_album(self):
item = self.lib.items().get()
self.assertEqual(item.title, u't\xeftle 0')
self.assertExists(item.path)
self.importer.default_resolution = self.importer.Resolution.REMOVE
self.importer.run()
self.assertNotExists(item.path)
self.assertEqual(len(self.lib.albums()), 1)
self.assertEqual(len(self.lib.items()), 1)
item = self.lib.items().get()
self.assertEqual(item.title, u'new title')
def test_no_autotag_keeps_duplicate_album(self):
config['import']['autotag'] = False
item = self.lib.items().get()
self.assertEqual(item.title, u't\xeftle 0')
self.assertExists(item.path)
# Imported item has the same artist and album as the one in the
# library.
import_file = os.path.join(self.importer.paths[0],
b'album 0', b'track 0.mp3')
import_file = MediaFile(import_file)
import_file.artist = item['artist']
import_file.albumartist = item['artist']
import_file.album = item['album']
import_file.title = 'new title'
self.importer.default_resolution = self.importer.Resolution.REMOVE
self.importer.run()
self.assertExists(item.path)
self.assertEqual(len(self.lib.albums()), 2)
self.assertEqual(len(self.lib.items()), 2)
def test_keep_duplicate_album(self):
self.importer.default_resolution = self.importer.Resolution.KEEPBOTH
self.importer.run()
self.assertEqual(len(self.lib.albums()), 2)
self.assertEqual(len(self.lib.items()), 2)
def test_skip_duplicate_album(self):
item = self.lib.items().get()
self.assertEqual(item.title, u't\xeftle 0')
self.importer.default_resolution = self.importer.Resolution.SKIP
self.importer.run()
self.assertEqual(len(self.lib.albums()), 1)
self.assertEqual(len(self.lib.items()), 1)
item = self.lib.items().get()
self.assertEqual(item.title, u't\xeftle 0')
def test_merge_duplicate_album(self):
self.importer.default_resolution = self.importer.Resolution.MERGE
self.importer.run()
self.assertEqual(len(self.lib.albums()), 1)
def test_twice_in_import_dir(self):
self.skipTest('write me')
def add_album_fixture(self, **kwargs):
# TODO move this into upstream
album = super(ImportDuplicateAlbumTest, self).add_album_fixture()
album.update(kwargs)
album.store()
return album
def test_track_info(*args, **kwargs):
return iter([TrackInfo(
artist=u'artist', title=u'title',
track_id=u'new trackid', index=0,)])
@patch('beets.autotag.mb.match_track', Mock(side_effect=test_track_info))
class ImportDuplicateSingletonTest(unittest.TestCase, TestHelper,
_common.Assertions):
def setUp(self):
self.setup_beets()
# Original file in library
self.add_item_fixture(artist=u'artist', title=u'title',
mb_trackid='old trackid')
# Import session
self.importer = self.create_importer()
config['import']['autotag'] = True
config['import']['singletons'] = True
def tearDown(self):
self.teardown_beets()
def test_remove_duplicate(self):
item = self.lib.items().get()
self.assertEqual(item.mb_trackid, u'old trackid')
self.assertExists(item.path)
self.importer.default_resolution = self.importer.Resolution.REMOVE
self.importer.run()
self.assertNotExists(item.path)
self.assertEqual(len(self.lib.items()), 1)
item = self.lib.items().get()
self.assertEqual(item.mb_trackid, u'new trackid')
def test_keep_duplicate(self):
self.assertEqual(len(self.lib.items()), 1)
self.importer.default_resolution = self.importer.Resolution.KEEPBOTH
self.importer.run()
self.assertEqual(len(self.lib.items()), 2)
def test_skip_duplicate(self):
item = self.lib.items().get()
self.assertEqual(item.mb_trackid, u'old trackid')
self.importer.default_resolution = self.importer.Resolution.SKIP
self.importer.run()
self.assertEqual(len(self.lib.items()), 1)
item = self.lib.items().get()
self.assertEqual(item.mb_trackid, u'old trackid')
def test_twice_in_import_dir(self):
self.skipTest('write me')
def add_item_fixture(self, **kwargs):
# Move this to TestHelper
item = self.add_item_fixtures()[0]
item.update(kwargs)
item.store()
return item
class TagLogTest(_common.TestCase):
def test_tag_log_line(self):
sio = StringIO()
handler = logging.StreamHandler(sio)
session = _common.import_session(loghandler=handler)
session.tag_log('status', 'path')
self.assertIn('status path', sio.getvalue())
def test_tag_log_unicode(self):
sio = StringIO()
handler = logging.StreamHandler(sio)
session = _common.import_session(loghandler=handler)
session.tag_log('status', u'caf\xe9') # send unicode
self.assertIn(u'status caf\xe9', sio.getvalue())
class ResumeImportTest(unittest.TestCase, TestHelper):
def setUp(self):
self.setup_beets()
def tearDown(self):
self.teardown_beets()
@patch('beets.plugins.send')
def test_resume_album(self, plugins_send):
self.importer = self.create_importer(album_count=2)
self.config['import']['resume'] = True
# Aborts import after one album. This also ensures that we skip
# the first album in the second try.
def raise_exception(event, **kwargs):
if event == 'album_imported':
raise importer.ImportAbort
plugins_send.side_effect = raise_exception
self.importer.run()
self.assertEqual(len(self.lib.albums()), 1)
self.assertIsNotNone(self.lib.albums(u'album:album 0').get())
self.importer.run()
self.assertEqual(len(self.lib.albums()), 2)
self.assertIsNotNone(self.lib.albums(u'album:album 1').get())
@patch('beets.plugins.send')
def test_resume_singleton(self, plugins_send):
self.importer = self.create_importer(item_count=2)
self.config['import']['resume'] = True
self.config['import']['singletons'] = True
# Aborts import after one track. This also ensures that we skip
# the first album in the second try.
def raise_exception(event, **kwargs):
if event == 'item_imported':
raise importer.ImportAbort
plugins_send.side_effect = raise_exception
self.importer.run()
self.assertEqual(len(self.lib.items()), 1)
self.assertIsNotNone(self.lib.items(u'title:track 0').get())
self.importer.run()
self.assertEqual(len(self.lib.items()), 2)
self.assertIsNotNone(self.lib.items(u'title:track 1').get())
class IncrementalImportTest(unittest.TestCase, TestHelper):
def setUp(self):
self.setup_beets()
self.config['import']['incremental'] = True
def tearDown(self):
self.teardown_beets()
def test_incremental_album(self):
importer = self.create_importer(album_count=1)
importer.run()
# Change album name so the original file would be imported again
# if incremental was off.
album = self.lib.albums().get()
album['album'] = 'edited album'
album.store()
importer = self.create_importer(album_count=1)
importer.run()
self.assertEqual(len(self.lib.albums()), 2)
def test_incremental_item(self):
self.config['import']['singletons'] = True
importer = self.create_importer(item_count=1)
importer.run()
# Change track name so the original file would be imported again
# if incremental was off.
item = self.lib.items().get()
item['artist'] = 'edited artist'
item.store()
importer = self.create_importer(item_count=1)
importer.run()
self.assertEqual(len(self.lib.items()), 2)
def test_invalid_state_file(self):
importer = self.create_importer()
with open(self.config['statefile'].as_filename(), 'wb') as f:
f.write(b'000')
importer.run()
self.assertEqual(len(self.lib.albums()), 1)
def _mkmp3(path):
shutil.copyfile(os.path.join(_common.RSRC, b'min.mp3'), path)
class AlbumsInDirTest(_common.TestCase):
def setUp(self):
super(AlbumsInDirTest, self).setUp()
# create a directory structure for testing
self.base = os.path.abspath(os.path.join(self.temp_dir, b'tempdir'))
os.mkdir(self.base)
os.mkdir(os.path.join(self.base, b'album1'))
os.mkdir(os.path.join(self.base, b'album2'))
os.mkdir(os.path.join(self.base, b'more'))
os.mkdir(os.path.join(self.base, b'more', b'album3'))
os.mkdir(os.path.join(self.base, b'more', b'album4'))
_mkmp3(os.path.join(self.base, b'album1', b'album1song1.mp3'))
_mkmp3(os.path.join(self.base, b'album1', b'album1song2.mp3'))
_mkmp3(os.path.join(self.base, b'album2', b'album2song.mp3'))
_mkmp3(os.path.join(self.base, b'more', b'album3', b'album3song.mp3'))
_mkmp3(os.path.join(self.base, b'more', b'album4', b'album4song.mp3'))
def test_finds_all_albums(self):
albums = list(albums_in_dir(self.base))
self.assertEqual(len(albums), 4)
def test_separates_contents(self):
found = []
for _, album in albums_in_dir(self.base):
found.append(re.search(br'album(.)song', album[0]).group(1))
self.assertTrue(b'1' in found)
self.assertTrue(b'2' in found)
self.assertTrue(b'3' in found)
self.assertTrue(b'4' in found)
def test_finds_multiple_songs(self):
for _, album in albums_in_dir(self.base):
n = re.search(br'album(.)song', album[0]).group(1)
if n == b'1':
self.assertEqual(len(album), 2)
else:
self.assertEqual(len(album), 1)
class MultiDiscAlbumsInDirTest(_common.TestCase):
def create_music(self, files=True, ascii=True):
"""Create some music in multiple album directories.
`files` indicates whether to create the files (otherwise, only
directories are made). `ascii` indicates ACII-only filenames;
otherwise, we use Unicode names.
"""
self.base = os.path.abspath(os.path.join(self.temp_dir, b'tempdir'))
os.mkdir(self.base)
name = b'CAT' if ascii else util.bytestring_path(u'C\xc1T')
name_alt_case = b'CAt' if ascii else util.bytestring_path(u'C\xc1t')
self.dirs = [
# Nested album, multiple subdirs.
# Also, false positive marker in root dir, and subtitle for disc 3.
os.path.join(self.base, b'ABCD1234'),
os.path.join(self.base, b'ABCD1234', b'cd 1'),
os.path.join(self.base, b'ABCD1234', b'cd 3 - bonus'),
# Nested album, single subdir.
# Also, punctuation between marker and disc number.
os.path.join(self.base, b'album'),
os.path.join(self.base, b'album', b'cd _ 1'),
# Flattened album, case typo.
# Also, false positive marker in parent dir.
os.path.join(self.base, b'artist [CD5]'),
os.path.join(self.base, b'artist [CD5]', name + b' disc 1'),
os.path.join(self.base, b'artist [CD5]',
name_alt_case + b' disc 2'),
# Single disc album, sorted between CAT discs.
os.path.join(self.base, b'artist [CD5]', name + b'S'),
]
self.files = [
os.path.join(self.base, b'ABCD1234', b'cd 1', b'song1.mp3'),
os.path.join(self.base, b'ABCD1234',
b'cd 3 - bonus', b'song2.mp3'),
os.path.join(self.base, b'ABCD1234',
b'cd 3 - bonus', b'song3.mp3'),
os.path.join(self.base, b'album', b'cd _ 1', b'song4.mp3'),
os.path.join(self.base, b'artist [CD5]', name + b' disc 1',
b'song5.mp3'),
os.path.join(self.base, b'artist [CD5]',
name_alt_case + b' disc 2', b'song6.mp3'),
os.path.join(self.base, b'artist [CD5]', name + b'S',
b'song7.mp3'),
]
if not ascii:
self.dirs = [self._normalize_path(p) for p in self.dirs]
self.files = [self._normalize_path(p) for p in self.files]
for path in self.dirs:
os.mkdir(util.syspath(path))
if files:
for path in self.files:
_mkmp3(util.syspath(path))
def _normalize_path(self, path):
"""Normalize a path's Unicode combining form according to the
platform.
"""
path = path.decode('utf-8')
norm_form = 'NFD' if sys.platform == 'darwin' else 'NFC'
path = unicodedata.normalize(norm_form, path)
return path.encode('utf-8')
def test_coalesce_nested_album_multiple_subdirs(self):
self.create_music()
albums = list(albums_in_dir(self.base))
self.assertEqual(len(albums), 4)
root, items = albums[0]
self.assertEqual(root, self.dirs[0:3])
self.assertEqual(len(items), 3)
def test_coalesce_nested_album_single_subdir(self):
self.create_music()
albums = list(albums_in_dir(self.base))
root, items = albums[1]
self.assertEqual(root, self.dirs[3:5])
self.assertEqual(len(items), 1)
def test_coalesce_flattened_album_case_typo(self):
self.create_music()
albums = list(albums_in_dir(self.base))
root, items = albums[2]
self.assertEqual(root, self.dirs[6:8])
self.assertEqual(len(items), 2)
def test_single_disc_album(self):
self.create_music()
albums = list(albums_in_dir(self.base))
root, items = albums[3]
self.assertEqual(root, self.dirs[8:])
self.assertEqual(len(items), 1)
def test_do_not_yield_empty_album(self):
self.create_music(files=False)
albums = list(albums_in_dir(self.base))
self.assertEqual(len(albums), 0)
def test_single_disc_unicode(self):
self.create_music(ascii=False)
albums = list(albums_in_dir(self.base))
root, items = albums[3]
self.assertEqual(root, self.dirs[8:])
self.assertEqual(len(items), 1)
def test_coalesce_multiple_unicode(self):
self.create_music(ascii=False)
albums = list(albums_in_dir(self.base))
self.assertEqual(len(albums), 4)
root, items = albums[0]
self.assertEqual(root, self.dirs[0:3])
self.assertEqual(len(items), 3)
class ReimportTest(unittest.TestCase, ImportHelper, _common.Assertions):
"""Test "re-imports", in which the autotagging machinery is used for
music that's already in the library.
This works by importing new database entries for the same files and
replacing the old data with the new data. We also copy over flexible
attributes and the added date.
"""
def setUp(self):
self.setup_beets()
# The existing album.
album = self.add_album_fixture()
album.added = 4242.0
album.foo = u'bar' # Some flexible attribute.
album.store()
item = album.items().get()
item.baz = u'qux'
item.added = 4747.0
item.store()
# Set up an import pipeline with a "good" match.
self.matcher = AutotagStub().install()
self.matcher.matching = AutotagStub.GOOD
def tearDown(self):
self.teardown_beets()
self.matcher.restore()
def _setup_session(self, singletons=False):
self._setup_import_session(self._album().path, singletons=singletons)
self.importer.add_choice(importer.action.APPLY)
def _album(self):
return self.lib.albums().get()
def _item(self):
return self.lib.items().get()
def test_reimported_album_gets_new_metadata(self):
self._setup_session()
self.assertEqual(self._album().album, u'\xe4lbum')
self.importer.run()
self.assertEqual(self._album().album, u'the album')
def test_reimported_album_preserves_flexattr(self):
self._setup_session()
self.importer.run()
self.assertEqual(self._album().foo, u'bar')
def test_reimported_album_preserves_added(self):
self._setup_session()
self.importer.run()
self.assertEqual(self._album().added, 4242.0)
def test_reimported_album_preserves_item_flexattr(self):
self._setup_session()
self.importer.run()
self.assertEqual(self._item().baz, u'qux')
def test_reimported_album_preserves_item_added(self):
self._setup_session()
self.importer.run()
self.assertEqual(self._item().added, 4747.0)
def test_reimported_item_gets_new_metadata(self):
self._setup_session(True)
self.assertEqual(self._item().title, u't\xeftle 0')
self.importer.run()
self.assertEqual(self._item().title, u'full')
def test_reimported_item_preserves_flexattr(self):
self._setup_session(True)
self.importer.run()
self.assertEqual(self._item().baz, u'qux')
def test_reimported_item_preserves_added(self):
self._setup_session(True)
self.importer.run()
self.assertEqual(self._item().added, 4747.0)
def test_reimported_item_preserves_art(self):
self._setup_session()
art_source = os.path.join(_common.RSRC, b'abbey.jpg')
replaced_album = self._album()
replaced_album.set_art(art_source)
replaced_album.store()
old_artpath = replaced_album.artpath
self.importer.run()
new_album = self._album()
new_artpath = new_album.art_destination(art_source)
self.assertEqual(new_album.artpath, new_artpath)
self.assertExists(new_artpath)
if new_artpath != old_artpath:
self.assertNotExists(old_artpath)
class ImportPretendTest(_common.TestCase, ImportHelper):
""" Test the pretend commandline option
"""
def __init__(self, method_name='runTest'):
super(ImportPretendTest, self).__init__(method_name)
self.matcher = None
def setUp(self):
super(ImportPretendTest, self).setUp()
self.setup_beets()
self.__create_import_dir()
self.__create_empty_import_dir()
self._setup_import_session()
config['import']['pretend'] = True
self.matcher = AutotagStub().install()
self.io.install()
def tearDown(self):
self.teardown_beets()
self.matcher.restore()
def __create_import_dir(self):
self._create_import_dir(1)
resource_path = os.path.join(_common.RSRC, b'empty.mp3')
single_path = os.path.join(self.import_dir, b'track_2.mp3')
shutil.copy(resource_path, single_path)
self.import_paths = [
os.path.join(self.import_dir, b'the_album'),
single_path
]
self.import_files = [
displayable_path(
os.path.join(self.import_paths[0], b'track_1.mp3')),
displayable_path(single_path)
]
def __create_empty_import_dir(self):
path = os.path.join(self.temp_dir, b'empty')
os.makedirs(path)
self.empty_path = path
def __run(self, import_paths, singletons=True):
self._setup_import_session(singletons=singletons)
self.importer.paths = import_paths
with capture_log() as logs:
self.importer.run()
logs = [line for line in logs if not line.startswith('Sending event:')]
self.assertEqual(len(self.lib.items()), 0)
self.assertEqual(len(self.lib.albums()), 0)
return logs
def test_import_singletons_pretend(self):
logs = self.__run(self.import_paths)
self.assertEqual(logs, [
'Singleton: %s' % displayable_path(self.import_files[0]),
'Singleton: %s' % displayable_path(self.import_paths[1])])
def test_import_album_pretend(self):
logs = self.__run(self.import_paths, singletons=False)
self.assertEqual(logs, [
'Album: %s' % displayable_path(self.import_paths[0]),
' %s' % displayable_path(self.import_files[0]),
'Album: %s' % displayable_path(self.import_paths[1]),
' %s' % displayable_path(self.import_paths[1])])
def test_import_pretend_empty(self):
logs = self.__run([self.empty_path])
self.assertEqual(logs, [u'No files imported from {0}'
.format(displayable_path(self.empty_path))])
# Helpers for ImportMusicBrainzIdTest.
def mocked_get_release_by_id(id_, includes=[], release_status=[],
release_type=[]):
"""Mimic musicbrainzngs.get_release_by_id, accepting only a restricted list
of MB ids (ID_RELEASE_0, ID_RELEASE_1). The returned dict differs only in
the release title and artist name, so that ID_RELEASE_0 is a closer match
to the items created by ImportHelper._create_import_dir()."""
# Map IDs to (release title, artist), so the distances are different.
releases = {ImportMusicBrainzIdTest.ID_RELEASE_0: ('VALID_RELEASE_0',
'TAG ARTIST'),
ImportMusicBrainzIdTest.ID_RELEASE_1: ('VALID_RELEASE_1',
'DISTANT_MATCH')}
return {
'release': {
'title': releases[id_][0],
'id': id_,
'medium-list': [{
'track-list': [{
'recording': {
'title': 'foo',
'id': 'bar',
'length': 59,
},
'position': 9,
'number': 'A2'
}],
'position': 5,
}],
'artist-credit': [{
'artist': {
'name': releases[id_][1],
'id': 'some-id',
},
}],
'release-group': {
'id': 'another-id',
}
}
}
def mocked_get_recording_by_id(id_, includes=[], release_status=[],
release_type=[]):
"""Mimic musicbrainzngs.get_recording_by_id, accepting only a restricted
list of MB ids (ID_RECORDING_0, ID_RECORDING_1). The returned dict differs
only in the recording title and artist name, so that ID_RECORDING_0 is a
closer match to the items created by ImportHelper._create_import_dir()."""
# Map IDs to (recording title, artist), so the distances are different.
releases = {ImportMusicBrainzIdTest.ID_RECORDING_0: ('VALID_RECORDING_0',
'TAG ARTIST'),
ImportMusicBrainzIdTest.ID_RECORDING_1: ('VALID_RECORDING_1',
'DISTANT_MATCH')}
return {
'recording': {
'title': releases[id_][0],
'id': id_,
'length': 59,
'artist-credit': [{
'artist': {
'name': releases[id_][1],
'id': 'some-id',
},
}],
}
}
@patch('musicbrainzngs.get_recording_by_id',
Mock(side_effect=mocked_get_recording_by_id))
@patch('musicbrainzngs.get_release_by_id',
Mock(side_effect=mocked_get_release_by_id))
class ImportMusicBrainzIdTest(_common.TestCase, ImportHelper):
"""Test the --musicbrainzid argument."""
MB_RELEASE_PREFIX = 'https://musicbrainz.org/release/'
MB_RECORDING_PREFIX = 'https://musicbrainz.org/recording/'
ID_RELEASE_0 = '00000000-0000-0000-0000-000000000000'
ID_RELEASE_1 = '11111111-1111-1111-1111-111111111111'
ID_RECORDING_0 = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
ID_RECORDING_1 = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'
def setUp(self):
self.setup_beets()
self._create_import_dir(1)
def tearDown(self):
self.teardown_beets()
def test_one_mbid_one_album(self):
self.config['import']['search_ids'] = \
[self.MB_RELEASE_PREFIX + self.ID_RELEASE_0]
self._setup_import_session()
self.importer.add_choice(importer.action.APPLY)
self.importer.run()
self.assertEqual(self.lib.albums().get().album, 'VALID_RELEASE_0')
def test_several_mbid_one_album(self):
self.config['import']['search_ids'] = \
[self.MB_RELEASE_PREFIX + self.ID_RELEASE_0,
self.MB_RELEASE_PREFIX + self.ID_RELEASE_1]
self._setup_import_session()
self.importer.add_choice(2) # Pick the 2nd best match (release 1).
self.importer.add_choice(importer.action.APPLY)
self.importer.run()
self.assertEqual(self.lib.albums().get().album, 'VALID_RELEASE_1')
def test_one_mbid_one_singleton(self):
self.config['import']['search_ids'] = \
[self.MB_RECORDING_PREFIX + self.ID_RECORDING_0]
self._setup_import_session(singletons=True)
self.importer.add_choice(importer.action.APPLY)
self.importer.run()
self.assertEqual(self.lib.items().get().title, 'VALID_RECORDING_0')
def test_several_mbid_one_singleton(self):
self.config['import']['search_ids'] = \
[self.MB_RECORDING_PREFIX + self.ID_RECORDING_0,
self.MB_RECORDING_PREFIX + self.ID_RECORDING_1]
self._setup_import_session(singletons=True)
self.importer.add_choice(2) # Pick the 2nd best match (recording 1).
self.importer.add_choice(importer.action.APPLY)
self.importer.run()
self.assertEqual(self.lib.items().get().title, 'VALID_RECORDING_1')
def test_candidates_album(self):
"""Test directly ImportTask.lookup_candidates()."""
task = importer.ImportTask(paths=self.import_dir,
toppath='top path',
items=[_common.item()])
task.search_ids = [self.MB_RELEASE_PREFIX + self.ID_RELEASE_0,
self.MB_RELEASE_PREFIX + self.ID_RELEASE_1,
'an invalid and discarded id']
task.lookup_candidates()
self.assertEqual(set(['VALID_RELEASE_0', 'VALID_RELEASE_1']),
set([c.info.album for c in task.candidates]))
def test_candidates_singleton(self):
"""Test directly SingletonImportTask.lookup_candidates()."""
task = importer.SingletonImportTask(toppath='top path',
item=_common.item())
task.search_ids = [self.MB_RECORDING_PREFIX + self.ID_RECORDING_0,
self.MB_RECORDING_PREFIX + self.ID_RECORDING_1,
'an invalid and discarded id']
task.lookup_candidates()
self.assertEqual(set(['VALID_RECORDING_0', 'VALID_RECORDING_1']),
set([c.info.title for c in task.candidates]))
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/__init__.py 0000644 0000765 0000024 00000000175 13025125202 016653 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
# Make python -m testall.py work.
from __future__ import division, absolute_import, print_function
beets-1.4.6/test/test_ui.py 0000644 0000765 0000024 00000141537 13164763003 016613 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Tests for the command-line interface.
"""
from __future__ import division, absolute_import, print_function
import os
import shutil
import re
import subprocess
import platform
from copy import deepcopy
import six
import unittest
from mock import patch, Mock
from test import _common
from test.helper import capture_stdout, has_program, TestHelper, control_stdin
from beets import library
from beets import ui
from beets.ui import commands
from beets import autotag
from beets.autotag.match import distance
from beets.mediafile import MediaFile
from beets import config
from beets import plugins
from beets.util.confit import ConfigError
from beets import util
from beets.util import syspath, MoveOperation
class ListTest(unittest.TestCase):
def setUp(self):
self.lib = library.Library(':memory:')
self.item = _common.item()
self.item.path = 'xxx/yyy'
self.lib.add(self.item)
self.lib.add_album([self.item])
def _run_list(self, query=u'', album=False, path=False, fmt=u''):
with capture_stdout() as stdout:
commands.list_items(self.lib, query, album, fmt)
return stdout
def test_list_outputs_item(self):
stdout = self._run_list()
self.assertIn(u'the title', stdout.getvalue())
def test_list_unicode_query(self):
self.item.title = u'na\xefve'
self.item.store()
self.lib._connection().commit()
stdout = self._run_list([u'na\xefve'])
out = stdout.getvalue()
if six.PY2:
out = out.decode(stdout.encoding)
self.assertTrue(u'na\xefve' in out)
def test_list_item_path(self):
stdout = self._run_list(fmt=u'$path')
self.assertEqual(stdout.getvalue().strip(), u'xxx/yyy')
def test_list_album_outputs_something(self):
stdout = self._run_list(album=True)
self.assertGreater(len(stdout.getvalue()), 0)
def test_list_album_path(self):
stdout = self._run_list(album=True, fmt=u'$path')
self.assertEqual(stdout.getvalue().strip(), u'xxx')
def test_list_album_omits_title(self):
stdout = self._run_list(album=True)
self.assertNotIn(u'the title', stdout.getvalue())
def test_list_uses_track_artist(self):
stdout = self._run_list()
self.assertIn(u'the artist', stdout.getvalue())
self.assertNotIn(u'the album artist', stdout.getvalue())
def test_list_album_uses_album_artist(self):
stdout = self._run_list(album=True)
self.assertNotIn(u'the artist', stdout.getvalue())
self.assertIn(u'the album artist', stdout.getvalue())
def test_list_item_format_artist(self):
stdout = self._run_list(fmt=u'$artist')
self.assertIn(u'the artist', stdout.getvalue())
def test_list_item_format_multiple(self):
stdout = self._run_list(fmt=u'$artist - $album - $year')
self.assertEqual(u'the artist - the album - 0001',
stdout.getvalue().strip())
def test_list_album_format(self):
stdout = self._run_list(album=True, fmt=u'$genre')
self.assertIn(u'the genre', stdout.getvalue())
self.assertNotIn(u'the album', stdout.getvalue())
class RemoveTest(_common.TestCase):
def setUp(self):
super(RemoveTest, self).setUp()
self.io.install()
self.libdir = os.path.join(self.temp_dir, b'testlibdir')
os.mkdir(self.libdir)
# Copy a file into the library.
self.lib = library.Library(':memory:', self.libdir)
item_path = os.path.join(_common.RSRC, b'full.mp3')
self.i = library.Item.from_path(item_path)
self.lib.add(self.i)
self.i.move(operation=MoveOperation.COPY)
def test_remove_items_no_delete(self):
self.io.addinput('y')
commands.remove_items(self.lib, u'', False, False, False)
items = self.lib.items()
self.assertEqual(len(list(items)), 0)
self.assertTrue(os.path.exists(self.i.path))
def test_remove_items_with_delete(self):
self.io.addinput('y')
commands.remove_items(self.lib, u'', False, True, False)
items = self.lib.items()
self.assertEqual(len(list(items)), 0)
self.assertFalse(os.path.exists(self.i.path))
def test_remove_items_with_force_no_delete(self):
commands.remove_items(self.lib, u'', False, False, True)
items = self.lib.items()
self.assertEqual(len(list(items)), 0)
self.assertTrue(os.path.exists(self.i.path))
def test_remove_items_with_force_delete(self):
commands.remove_items(self.lib, u'', False, True, True)
items = self.lib.items()
self.assertEqual(len(list(items)), 0)
self.assertFalse(os.path.exists(self.i.path))
class ModifyTest(unittest.TestCase, TestHelper):
def setUp(self):
self.setup_beets()
self.album = self.add_album_fixture()
[self.item] = self.album.items()
def tearDown(self):
self.teardown_beets()
def modify_inp(self, inp, *args):
with control_stdin(inp):
self.run_command('modify', *args)
def modify(self, *args):
self.modify_inp('y', *args)
# Item tests
def test_modify_item(self):
self.modify(u"title=newTitle")
item = self.lib.items().get()
self.assertEqual(item.title, u'newTitle')
def test_modify_item_abort(self):
item = self.lib.items().get()
title = item.title
self.modify_inp('n', u"title=newTitle")
item = self.lib.items().get()
self.assertEqual(item.title, title)
def test_modify_item_no_change(self):
title = u"Tracktitle"
item = self.add_item_fixture(title=title)
self.modify_inp('y', u"title", u"title={0}".format(title))
item = self.lib.items(title).get()
self.assertEqual(item.title, title)
def test_modify_write_tags(self):
self.modify(u"title=newTitle")
item = self.lib.items().get()
item.read()
self.assertEqual(item.title, u'newTitle')
def test_modify_dont_write_tags(self):
self.modify(u"--nowrite", u"title=newTitle")
item = self.lib.items().get()
item.read()
self.assertNotEqual(item.title, 'newTitle')
def test_move(self):
self.modify(u"title=newTitle")
item = self.lib.items().get()
self.assertIn(b'newTitle', item.path)
def test_not_move(self):
self.modify(u"--nomove", u"title=newTitle")
item = self.lib.items().get()
self.assertNotIn(b'newTitle', item.path)
def test_no_write_no_move(self):
self.modify(u"--nomove", u"--nowrite", u"title=newTitle")
item = self.lib.items().get()
item.read()
self.assertNotIn(b'newTitle', item.path)
self.assertNotEqual(item.title, u'newTitle')
def test_update_mtime(self):
item = self.item
old_mtime = item.mtime
self.modify(u"title=newTitle")
item.load()
self.assertNotEqual(old_mtime, item.mtime)
self.assertEqual(item.current_mtime(), item.mtime)
def test_reset_mtime_with_no_write(self):
item = self.item
self.modify(u"--nowrite", u"title=newTitle")
item.load()
self.assertEqual(0, item.mtime)
def test_selective_modify(self):
title = u"Tracktitle"
album = u"album"
original_artist = u"composer"
new_artist = u"coverArtist"
for i in range(0, 10):
self.add_item_fixture(title=u"{0}{1}".format(title, i),
artist=original_artist,
album=album)
self.modify_inp('s\ny\ny\ny\nn\nn\ny\ny\ny\ny\nn',
title, u"artist={0}".format(new_artist))
original_items = self.lib.items(u"artist:{0}".format(original_artist))
new_items = self.lib.items(u"artist:{0}".format(new_artist))
self.assertEqual(len(list(original_items)), 3)
self.assertEqual(len(list(new_items)), 7)
# Album Tests
def test_modify_album(self):
self.modify(u"--album", u"album=newAlbum")
album = self.lib.albums().get()
self.assertEqual(album.album, u'newAlbum')
def test_modify_album_write_tags(self):
self.modify(u"--album", u"album=newAlbum")
item = self.lib.items().get()
item.read()
self.assertEqual(item.album, u'newAlbum')
def test_modify_album_dont_write_tags(self):
self.modify(u"--album", u"--nowrite", u"album=newAlbum")
item = self.lib.items().get()
item.read()
self.assertEqual(item.album, u'the album')
def test_album_move(self):
self.modify(u"--album", u"album=newAlbum")
item = self.lib.items().get()
item.read()
self.assertIn(b'newAlbum', item.path)
def test_album_not_move(self):
self.modify(u"--nomove", u"--album", u"album=newAlbum")
item = self.lib.items().get()
item.read()
self.assertNotIn(b'newAlbum', item.path)
# Misc
def test_write_initial_key_tag(self):
self.modify(u"initial_key=C#m")
item = self.lib.items().get()
mediafile = MediaFile(syspath(item.path))
self.assertEqual(mediafile.initial_key, u'C#m')
def test_set_flexattr(self):
self.modify(u"flexattr=testAttr")
item = self.lib.items().get()
self.assertEqual(item.flexattr, u'testAttr')
def test_remove_flexattr(self):
item = self.lib.items().get()
item.flexattr = u'testAttr'
item.store()
self.modify(u"flexattr!")
item = self.lib.items().get()
self.assertNotIn(u"flexattr", item)
@unittest.skip(u'not yet implemented')
def test_delete_initial_key_tag(self):
item = self.lib.items().get()
item.initial_key = u'C#m'
item.write()
item.store()
mediafile = MediaFile(syspath(item.path))
self.assertEqual(mediafile.initial_key, u'C#m')
self.modify(u"initial_key!")
mediafile = MediaFile(syspath(item.path))
self.assertIsNone(mediafile.initial_key)
def test_arg_parsing_colon_query(self):
(query, mods, dels) = commands.modify_parse_args([u"title:oldTitle",
u"title=newTitle"])
self.assertEqual(query, [u"title:oldTitle"])
self.assertEqual(mods, {"title": u"newTitle"})
def test_arg_parsing_delete(self):
(query, mods, dels) = commands.modify_parse_args([u"title:oldTitle",
u"title!"])
self.assertEqual(query, [u"title:oldTitle"])
self.assertEqual(dels, ["title"])
def test_arg_parsing_query_with_exclaimation(self):
(query, mods, dels) = commands.modify_parse_args([u"title:oldTitle!",
u"title=newTitle!"])
self.assertEqual(query, [u"title:oldTitle!"])
self.assertEqual(mods, {"title": u"newTitle!"})
def test_arg_parsing_equals_in_value(self):
(query, mods, dels) = commands.modify_parse_args([u"title:foo=bar",
u"title=newTitle"])
self.assertEqual(query, [u"title:foo=bar"])
self.assertEqual(mods, {"title": u"newTitle"})
class WriteTest(unittest.TestCase, TestHelper):
def setUp(self):
self.setup_beets()
def tearDown(self):
self.teardown_beets()
def write_cmd(self, *args):
return self.run_with_output('write', *args)
def test_update_mtime(self):
item = self.add_item_fixture()
item['title'] = u'a new title'
item.store()
item = self.lib.items().get()
self.assertEqual(item.mtime, 0)
self.write_cmd()
item = self.lib.items().get()
self.assertEqual(item.mtime, item.current_mtime())
def test_non_metadata_field_unchanged(self):
"""Changing a non-"tag" field like `bitrate` and writing should
have no effect.
"""
# An item that starts out "clean".
item = self.add_item_fixture()
item.read()
# ... but with a mismatched bitrate.
item.bitrate = 123
item.store()
output = self.write_cmd()
self.assertEqual(output, '')
def test_write_metadata_field(self):
item = self.add_item_fixture()
item.read()
old_title = item.title
item.title = u'new title'
item.store()
output = self.write_cmd()
self.assertTrue(u'{0} -> new title'.format(old_title)
in output)
class MoveTest(_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=False):
commands.move_items(self.lib, dest, query, copy, album, pretend,
export=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)
def test_export_item_custom_dir(self):
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, 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, export=True)
self.i.load()
self.assertIn(b'srcfile', self.i.path)
self.assertNotExists(self.otherdir)
class UpdateTest(_common.TestCase):
def setUp(self):
super(UpdateTest, self).setUp()
self.io.install()
self.libdir = os.path.join(self.temp_dir, b'testlibdir')
# Copy a file into the library.
self.lib = library.Library(':memory:', self.libdir)
item_path = os.path.join(_common.RSRC, b'full.mp3')
self.i = library.Item.from_path(item_path)
self.lib.add(self.i)
self.i.move(operation=MoveOperation.COPY)
self.album = self.lib.add_album([self.i])
# Album art.
artfile = os.path.join(self.temp_dir, b'testart.jpg')
_common.touch(artfile)
self.album.set_art(artfile)
self.album.store()
os.remove(artfile)
def _update(self, query=(), album=False, move=False, reset_mtime=True,
fields=None):
self.io.addinput('y')
if reset_mtime:
self.i.mtime = 0
self.i.store()
commands.update_items(self.lib, query, album, move, False,
fields=fields)
def test_delete_removes_item(self):
self.assertTrue(list(self.lib.items()))
os.remove(self.i.path)
self._update()
self.assertFalse(list(self.lib.items()))
def test_delete_removes_album(self):
self.assertTrue(self.lib.albums())
os.remove(self.i.path)
self._update()
self.assertFalse(self.lib.albums())
def test_delete_removes_album_art(self):
artpath = self.album.artpath
self.assertExists(artpath)
os.remove(self.i.path)
self._update()
self.assertNotExists(artpath)
def test_modified_metadata_detected(self):
mf = MediaFile(syspath(self.i.path))
mf.title = u'differentTitle'
mf.save()
self._update()
item = self.lib.items().get()
self.assertEqual(item.title, u'differentTitle')
def test_modified_metadata_moved(self):
mf = MediaFile(syspath(self.i.path))
mf.title = u'differentTitle'
mf.save()
self._update(move=True)
item = self.lib.items().get()
self.assertTrue(b'differentTitle' in item.path)
def test_modified_metadata_not_moved(self):
mf = MediaFile(syspath(self.i.path))
mf.title = u'differentTitle'
mf.save()
self._update(move=False)
item = self.lib.items().get()
self.assertTrue(b'differentTitle' not in item.path)
def test_selective_modified_metadata_moved(self):
mf = MediaFile(syspath(self.i.path))
mf.title = u'differentTitle'
mf.genre = u'differentGenre'
mf.save()
self._update(move=True, fields=['title'])
item = self.lib.items().get()
self.assertTrue(b'differentTitle' in item.path)
self.assertNotEqual(item.genre, u'differentGenre')
def test_selective_modified_metadata_not_moved(self):
mf = MediaFile(syspath(self.i.path))
mf.title = u'differentTitle'
mf.genre = u'differentGenre'
mf.save()
self._update(move=False, fields=['title'])
item = self.lib.items().get()
self.assertTrue(b'differentTitle' not in item.path)
self.assertNotEqual(item.genre, u'differentGenre')
def test_modified_album_metadata_moved(self):
mf = MediaFile(syspath(self.i.path))
mf.album = u'differentAlbum'
mf.save()
self._update(move=True)
item = self.lib.items().get()
self.assertTrue(b'differentAlbum' in item.path)
def test_modified_album_metadata_art_moved(self):
artpath = self.album.artpath
mf = MediaFile(syspath(self.i.path))
mf.album = u'differentAlbum'
mf.save()
self._update(move=True)
album = self.lib.albums()[0]
self.assertNotEqual(artpath, album.artpath)
def test_selective_modified_album_metadata_moved(self):
mf = MediaFile(syspath(self.i.path))
mf.album = u'differentAlbum'
mf.genre = u'differentGenre'
mf.save()
self._update(move=True, fields=['album'])
item = self.lib.items().get()
self.assertTrue(b'differentAlbum' in item.path)
self.assertNotEqual(item.genre, u'differentGenre')
def test_selective_modified_album_metadata_not_moved(self):
mf = MediaFile(syspath(self.i.path))
mf.album = u'differentAlbum'
mf.genre = u'differentGenre'
mf.save()
self._update(move=True, fields=['genre'])
item = self.lib.items().get()
self.assertTrue(b'differentAlbum' not in item.path)
self.assertEqual(item.genre, u'differentGenre')
def test_mtime_match_skips_update(self):
mf = MediaFile(syspath(self.i.path))
mf.title = u'differentTitle'
mf.save()
# Make in-memory mtime match on-disk mtime.
self.i.mtime = os.path.getmtime(self.i.path)
self.i.store()
self._update(reset_mtime=False)
item = self.lib.items().get()
self.assertEqual(item.title, u'full')
class PrintTest(_common.TestCase):
def setUp(self):
super(PrintTest, self).setUp()
self.io.install()
def test_print_without_locale(self):
lang = os.environ.get('LANG')
if lang:
del os.environ['LANG']
try:
ui.print_(u'something')
except TypeError:
self.fail(u'TypeError during print')
finally:
if lang:
os.environ['LANG'] = lang
def test_print_with_invalid_locale(self):
old_lang = os.environ.get('LANG')
os.environ['LANG'] = ''
old_ctype = os.environ.get('LC_CTYPE')
os.environ['LC_CTYPE'] = 'UTF-8'
try:
ui.print_(u'something')
except ValueError:
self.fail(u'ValueError during print')
finally:
if old_lang:
os.environ['LANG'] = old_lang
else:
del os.environ['LANG']
if old_ctype:
os.environ['LC_CTYPE'] = old_ctype
else:
del os.environ['LC_CTYPE']
class ImportTest(_common.TestCase):
def test_quiet_timid_disallowed(self):
config['import']['quiet'] = True
config['import']['timid'] = True
self.assertRaises(ui.UserError, commands.import_files, None, [],
None)
@_common.slow_test()
class ConfigTest(unittest.TestCase, TestHelper, _common.Assertions):
def setUp(self):
self.setup_beets()
# Don't use the BEETSDIR from `helper`. Instead, we point the home
# directory there. Some tests will set `BEETSDIR` themselves.
del os.environ['BEETSDIR']
self._old_home = os.environ.get('HOME')
os.environ['HOME'] = util.py3_path(self.temp_dir)
# Also set APPDATA, the Windows equivalent of setting $HOME.
self._old_appdata = os.environ.get('APPDATA')
os.environ['APPDATA'] = \
util.py3_path(os.path.join(self.temp_dir, b'AppData', b'Roaming'))
self._orig_cwd = os.getcwd()
self.test_cmd = self._make_test_cmd()
commands.default_commands.append(self.test_cmd)
# Default user configuration
if platform.system() == 'Windows':
self.user_config_dir = os.path.join(
self.temp_dir, b'AppData', b'Roaming', b'beets'
)
else:
self.user_config_dir = os.path.join(
self.temp_dir, b'.config', b'beets'
)
os.makedirs(self.user_config_dir)
self.user_config_path = os.path.join(self.user_config_dir,
b'config.yaml')
# Custom BEETSDIR
self.beetsdir = os.path.join(self.temp_dir, b'beetsdir')
os.makedirs(self.beetsdir)
self._reset_config()
def tearDown(self):
commands.default_commands.pop()
os.chdir(self._orig_cwd)
if self._old_home is not None:
os.environ['HOME'] = self._old_home
if self._old_appdata is None:
del os.environ['APPDATA']
else:
os.environ['APPDATA'] = self._old_appdata
self.teardown_beets()
def _make_test_cmd(self):
test_cmd = ui.Subcommand('test', help=u'test')
def run(lib, options, args):
test_cmd.lib = lib
test_cmd.options = options
test_cmd.args = args
test_cmd.func = run
return test_cmd
def _reset_config(self):
# Config should read files again on demand
config.clear()
config._materialized = False
def write_config_file(self):
return open(self.user_config_path, 'w')
def test_paths_section_respected(self):
with self.write_config_file() as config:
config.write('paths: {x: y}')
self.run_command('test', lib=None)
key, template = self.test_cmd.lib.path_formats[0]
self.assertEqual(key, 'x')
self.assertEqual(template.original, 'y')
def test_default_paths_preserved(self):
default_formats = ui.get_path_formats()
self._reset_config()
with self.write_config_file() as config:
config.write('paths: {x: y}')
self.run_command('test', lib=None)
key, template = self.test_cmd.lib.path_formats[0]
self.assertEqual(key, 'x')
self.assertEqual(template.original, 'y')
self.assertEqual(self.test_cmd.lib.path_formats[1:],
default_formats)
def test_nonexistant_db(self):
with self.write_config_file() as config:
config.write('library: /xxx/yyy/not/a/real/path')
with self.assertRaises(ui.UserError):
self.run_command('test', lib=None)
def test_user_config_file(self):
with self.write_config_file() as file:
file.write('anoption: value')
self.run_command('test', lib=None)
self.assertEqual(config['anoption'].get(), 'value')
def test_replacements_parsed(self):
with self.write_config_file() as config:
config.write("replace: {'[xy]': z}")
self.run_command('test', lib=None)
replacements = self.test_cmd.lib.replacements
self.assertEqual(replacements, [(re.compile(u'[xy]'), 'z')])
def test_multiple_replacements_parsed(self):
with self.write_config_file() as config:
config.write("replace: {'[xy]': z, foo: bar}")
self.run_command('test', lib=None)
replacements = self.test_cmd.lib.replacements
self.assertEqual(replacements, [
(re.compile(u'[xy]'), u'z'),
(re.compile(u'foo'), u'bar'),
])
def test_cli_config_option(self):
config_path = os.path.join(self.temp_dir, b'config.yaml')
with open(config_path, 'w') as file:
file.write('anoption: value')
self.run_command('--config', config_path, 'test', lib=None)
self.assertEqual(config['anoption'].get(), 'value')
def test_cli_config_file_overwrites_user_defaults(self):
with open(self.user_config_path, 'w') as file:
file.write('anoption: value')
cli_config_path = os.path.join(self.temp_dir, b'config.yaml')
with open(cli_config_path, 'w') as file:
file.write('anoption: cli overwrite')
self.run_command('--config', cli_config_path, 'test', lib=None)
self.assertEqual(config['anoption'].get(), 'cli overwrite')
def test_cli_config_file_overwrites_beetsdir_defaults(self):
os.environ['BEETSDIR'] = util.py3_path(self.beetsdir)
env_config_path = os.path.join(self.beetsdir, b'config.yaml')
with open(env_config_path, 'w') as file:
file.write('anoption: value')
cli_config_path = os.path.join(self.temp_dir, b'config.yaml')
with open(cli_config_path, 'w') as file:
file.write('anoption: cli overwrite')
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')
def test_cli_config_paths_resolve_relative_to_user_dir(self):
cli_config_path = os.path.join(self.temp_dir, b'config.yaml')
with open(cli_config_path, 'w') as file:
file.write('library: beets.db\n')
file.write('statefile: state')
self.run_command('--config', cli_config_path, 'test', lib=None)
self.assert_equal_path(
util.bytestring_path(config['library'].as_filename()),
os.path.join(self.user_config_dir, b'beets.db')
)
self.assert_equal_path(
util.bytestring_path(config['statefile'].as_filename()),
os.path.join(self.user_config_dir, b'state')
)
def test_cli_config_paths_resolve_relative_to_beetsdir(self):
os.environ['BEETSDIR'] = util.py3_path(self.beetsdir)
cli_config_path = os.path.join(self.temp_dir, b'config.yaml')
with open(cli_config_path, 'w') as file:
file.write('library: beets.db\n')
file.write('statefile: state')
self.run_command('--config', cli_config_path, 'test', lib=None)
self.assert_equal_path(
util.bytestring_path(config['library'].as_filename()),
os.path.join(self.beetsdir, b'beets.db')
)
self.assert_equal_path(
util.bytestring_path(config['statefile'].as_filename()),
os.path.join(self.beetsdir, b'state')
)
def test_command_line_option_relative_to_working_dir(self):
os.chdir(self.temp_dir)
self.run_command('--library', 'foo.db', 'test', lib=None)
self.assert_equal_path(config['library'].as_filename(),
os.path.join(os.getcwd(), 'foo.db'))
def test_cli_config_file_loads_plugin_commands(self):
cli_config_path = os.path.join(self.temp_dir, b'config.yaml')
with open(cli_config_path, 'w') as file:
file.write('pluginpath: %s\n' % _common.PLUGINPATH)
file.write('plugins: test')
self.run_command('--config', cli_config_path, 'plugin', lib=None)
self.assertTrue(plugins.find_plugins()[0].is_test_plugin)
def test_beetsdir_config(self):
os.environ['BEETSDIR'] = util.py3_path(self.beetsdir)
env_config_path = os.path.join(self.beetsdir, b'config.yaml')
with open(env_config_path, 'w') as file:
file.write('anoption: overwrite')
config.read()
self.assertEqual(config['anoption'].get(), 'overwrite')
def test_beetsdir_points_to_file_error(self):
beetsdir = os.path.join(self.temp_dir, b'beetsfile')
open(beetsdir, 'a').close()
os.environ['BEETSDIR'] = util.py3_path(beetsdir)
self.assertRaises(ConfigError, self.run_command, 'test')
def test_beetsdir_config_does_not_load_default_user_config(self):
os.environ['BEETSDIR'] = util.py3_path(self.beetsdir)
with open(self.user_config_path, 'w') as file:
file.write('anoption: value')
config.read()
self.assertFalse(config['anoption'].exists())
def test_default_config_paths_resolve_relative_to_beetsdir(self):
os.environ['BEETSDIR'] = util.py3_path(self.beetsdir)
config.read()
self.assert_equal_path(
util.bytestring_path(config['library'].as_filename()),
os.path.join(self.beetsdir, b'library.db')
)
self.assert_equal_path(
util.bytestring_path(config['statefile'].as_filename()),
os.path.join(self.beetsdir, b'state.pickle')
)
def test_beetsdir_config_paths_resolve_relative_to_beetsdir(self):
os.environ['BEETSDIR'] = util.py3_path(self.beetsdir)
env_config_path = os.path.join(self.beetsdir, b'config.yaml')
with open(env_config_path, 'w') as file:
file.write('library: beets.db\n')
file.write('statefile: state')
config.read()
self.assert_equal_path(
util.bytestring_path(config['library'].as_filename()),
os.path.join(self.beetsdir, b'beets.db')
)
self.assert_equal_path(
util.bytestring_path(config['statefile'].as_filename()),
os.path.join(self.beetsdir, b'state')
)
class ShowModelChangeTest(_common.TestCase):
def setUp(self):
super(ShowModelChangeTest, self).setUp()
self.io.install()
self.a = _common.item()
self.b = _common.item()
self.a.path = self.b.path
def _show(self, **kwargs):
change = ui.show_model_changes(self.a, self.b, **kwargs)
out = self.io.getoutput()
return change, out
def test_identical(self):
change, out = self._show()
self.assertFalse(change)
self.assertEqual(out, '')
def test_string_fixed_field_change(self):
self.b.title = 'x'
change, out = self._show()
self.assertTrue(change)
self.assertTrue(u'title' in out)
def test_int_fixed_field_change(self):
self.b.track = 9
change, out = self._show()
self.assertTrue(change)
self.assertTrue(u'track' in out)
def test_floats_close_to_identical(self):
self.a.length = 1.00001
self.b.length = 1.00005
change, out = self._show()
self.assertFalse(change)
self.assertEqual(out, u'')
def test_floats_different(self):
self.a.length = 1.00001
self.b.length = 2.00001
change, out = self._show()
self.assertTrue(change)
self.assertTrue(u'length' in out)
def test_both_values_shown(self):
self.a.title = u'foo'
self.b.title = u'bar'
change, out = self._show()
self.assertTrue(u'foo' in out)
self.assertTrue(u'bar' in out)
class ShowChangeTest(_common.TestCase):
def setUp(self):
super(ShowChangeTest, self).setUp()
self.io.install()
self.items = [_common.item()]
self.items[0].track = 1
self.items[0].path = b'/path/to/file.mp3'
self.info = autotag.AlbumInfo(
u'the album', u'album id', u'the artist', u'artist id', [
autotag.TrackInfo(u'the title', u'track id', index=1)
]
)
def _show_change(self, items=None, info=None,
cur_artist=u'the artist', cur_album=u'the album',
dist=0.1):
"""Return an unicode string representing the changes"""
items = items or self.items
info = info or self.info
mapping = dict(zip(items, info.tracks))
config['ui']['color'] = False
album_dist = distance(items, info, mapping)
album_dist._penalties = {'album': [dist]}
commands.show_change(
cur_artist,
cur_album,
autotag.AlbumMatch(album_dist, info, mapping, set(), set()),
)
# FIXME decoding shouldn't be done here
return util.text_string(self.io.getoutput().lower())
def test_null_change(self):
msg = self._show_change()
self.assertTrue('similarity: 90' in msg)
self.assertTrue('tagging:' in msg)
def test_album_data_change(self):
msg = self._show_change(cur_artist='another artist',
cur_album='another album')
self.assertTrue('correcting tags from:' in msg)
def test_item_data_change(self):
self.items[0].title = u'different'
msg = self._show_change()
self.assertTrue('different -> the title' in msg)
def test_item_data_change_with_unicode(self):
self.items[0].title = u'caf\xe9'
msg = self._show_change()
self.assertTrue(u'caf\xe9 -> the title' in msg)
def test_album_data_change_with_unicode(self):
msg = self._show_change(cur_artist=u'caf\xe9',
cur_album=u'another album')
self.assertTrue(u'correcting tags from:' in msg)
def test_item_data_change_title_missing(self):
self.items[0].title = u''
msg = re.sub(r' +', ' ', self._show_change())
self.assertTrue(u'file.mp3 -> the title' in msg)
def test_item_data_change_title_missing_with_unicode_filename(self):
self.items[0].title = u''
self.items[0].path = u'/path/to/caf\xe9.mp3'.encode('utf-8')
msg = re.sub(r' +', ' ', self._show_change())
self.assertTrue(u'caf\xe9.mp3 -> the title' in msg or
u'caf.mp3 ->' in msg)
@patch('beets.library.Item.try_filesize', Mock(return_value=987))
class SummarizeItemsTest(_common.TestCase):
def setUp(self):
super(SummarizeItemsTest, self).setUp()
item = library.Item()
item.bitrate = 4321
item.length = 10 * 60 + 54
item.format = "F"
self.item = item
def test_summarize_item(self):
summary = commands.summarize_items([], True)
self.assertEqual(summary, u"")
summary = commands.summarize_items([self.item], True)
self.assertEqual(summary, u"F, 4kbps, 10:54, 987.0 B")
def test_summarize_items(self):
summary = commands.summarize_items([], False)
self.assertEqual(summary, u"0 items")
summary = commands.summarize_items([self.item], False)
self.assertEqual(summary, u"1 items, F, 4kbps, 10:54, 987.0 B")
i2 = deepcopy(self.item)
summary = commands.summarize_items([self.item, i2], False)
self.assertEqual(summary, u"2 items, F, 4kbps, 21:48, 1.9 KiB")
i2.format = "G"
summary = commands.summarize_items([self.item, i2], False)
self.assertEqual(summary, u"2 items, F 1, G 1, 4kbps, 21:48, 1.9 KiB")
summary = commands.summarize_items([self.item, i2, i2], False)
self.assertEqual(summary, u"3 items, G 2, F 1, 4kbps, 32:42, 2.9 KiB")
class PathFormatTest(_common.TestCase):
def test_custom_paths_prepend(self):
default_formats = ui.get_path_formats()
config['paths'] = {u'foo': u'bar'}
pf = ui.get_path_formats()
key, tmpl = pf[0]
self.assertEqual(key, u'foo')
self.assertEqual(tmpl.original, u'bar')
self.assertEqual(pf[1:], default_formats)
@_common.slow_test()
class PluginTest(_common.TestCase, TestHelper):
def test_plugin_command_from_pluginpath(self):
config['pluginpath'] = [_common.PLUGINPATH]
config['plugins'] = ['test']
self.run_command('test', lib=None)
@_common.slow_test()
class CompletionTest(_common.TestCase, TestHelper):
def test_completion(self):
# Load plugin commands
config['pluginpath'] = [_common.PLUGINPATH]
config['plugins'] = ['test']
# Do not load any other bash completion scripts on the system.
env = dict(os.environ)
env['BASH_COMPLETION_DIR'] = os.devnull
env['BASH_COMPLETION_COMPAT_DIR'] = os.devnull
# Open a `bash` process to run the tests in. We'll pipe in bash
# commands via stdin.
cmd = os.environ.get('BEETS_TEST_SHELL', '/bin/bash --norc').split()
if not has_program(cmd[0]):
self.skipTest(u'bash not available')
tester = subprocess.Popen(cmd, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, env=env)
# Load bash_completion library.
for path in commands.BASH_COMPLETION_PATHS:
if os.path.exists(util.syspath(path)):
bash_completion = path
break
else:
self.skipTest(u'bash-completion script not found')
try:
with open(util.syspath(bash_completion), 'rb') as f:
tester.stdin.writelines(f)
except IOError:
self.skipTest(u'could not read bash-completion script')
# Load completion script.
self.io.install()
self.run_command('completion', lib=None)
completion_script = self.io.getoutput().encode('utf-8')
self.io.restore()
tester.stdin.writelines(completion_script.splitlines(True))
# Load test suite.
test_script_name = os.path.join(_common.RSRC, b'test_completion.sh')
with open(test_script_name, 'rb') as test_script_file:
tester.stdin.writelines(test_script_file)
out, err = tester.communicate()
if tester.returncode != 0 or out != b'completion tests passed\n':
print(out.decode('utf-8'))
self.fail(u'test/test_completion.sh did not execute properly')
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:')
self.item = _common.item()
self.item.path = b'xxx/yyy'
self.lib.add(self.item)
self.lib.add_album([self.item])
def tearDown(self):
self.teardown_beets()
def test_base(self):
l = self.run_with_output(u'ls')
self.assertEqual(l, u'the artist - the album - the title\n')
l = self.run_with_output(u'ls', u'-a')
self.assertEqual(l, u'the album artist - the album\n')
def test_path_option(self):
l = self.run_with_output(u'ls', u'-p')
self.assertEqual(l, u'xxx/yyy\n')
l = self.run_with_output(u'ls', u'-a', u'-p')
self.assertEqual(l, u'xxx\n')
def test_format_option(self):
l = self.run_with_output(u'ls', u'-f', u'$artist')
self.assertEqual(l, u'the artist\n')
l = self.run_with_output(u'ls', u'-a', u'-f', u'$albumartist')
self.assertEqual(l, u'the album artist\n')
def test_format_option_unicode(self):
l = self.run_with_output(b'ls', b'-f',
u'caf\xe9'.encode(util.arg_encoding()))
self.assertEqual(l, u'caf\xe9\n')
def test_root_format_option(self):
l = self.run_with_output(u'--format-item', u'$artist',
u'--format-album', u'foo', u'ls')
self.assertEqual(l, u'the artist\n')
l = self.run_with_output(u'--format-item', u'foo',
u'--format-album', u'$albumartist',
u'ls', u'-a')
self.assertEqual(l, u'the album artist\n')
def test_help(self):
l = self.run_with_output(u'help')
self.assertIn(u'Usage:', l)
l = self.run_with_output(u'help', u'list')
self.assertIn(u'Usage:', l)
with self.assertRaises(ui.UserError):
self.run_command(u'help', u'this.is.not.a.real.command')
def test_stats(self):
l = self.run_with_output(u'stats')
self.assertIn(u'Approximate total size:', l)
# # Need to have more realistic library setup for this to work
# l = self.run_with_output('stats', '-e')
# self.assertIn('Total size:', l)
def test_version(self):
l = self.run_with_output(u'version')
self.assertIn(u'Python version', l)
self.assertIn(u'no plugins loaded', l)
# # Need to have plugin loaded
# l = self.run_with_output('version')
# self.assertIn('plugins: ', l)
class CommonOptionsParserTest(unittest.TestCase, TestHelper):
def setUp(self):
self.setup_beets()
def tearDown(self):
self.teardown_beets()
def test_album_option(self):
parser = ui.CommonOptionsParser()
self.assertFalse(parser._album_flags)
parser.add_album_option()
self.assertTrue(bool(parser._album_flags))
self.assertEqual(parser.parse_args([]), ({'album': None}, []))
self.assertEqual(parser.parse_args([u'-a']), ({'album': True}, []))
self.assertEqual(parser.parse_args([u'--album']),
({'album': True}, []))
def test_path_option(self):
parser = ui.CommonOptionsParser()
parser.add_path_option()
self.assertFalse(parser._album_flags)
config['format_item'].set('$foo')
self.assertEqual(parser.parse_args([]), ({'path': None}, []))
self.assertEqual(config['format_item'].as_str(), u'$foo')
self.assertEqual(parser.parse_args([u'-p']),
({'path': True, 'format': u'$path'}, []))
self.assertEqual(parser.parse_args(['--path']),
({'path': True, 'format': u'$path'}, []))
self.assertEqual(config['format_item'].as_str(), u'$path')
self.assertEqual(config['format_album'].as_str(), u'$path')
def test_format_option(self):
parser = ui.CommonOptionsParser()
parser.add_format_option()
self.assertFalse(parser._album_flags)
config['format_item'].set('$foo')
self.assertEqual(parser.parse_args([]), ({'format': None}, []))
self.assertEqual(config['format_item'].as_str(), u'$foo')
self.assertEqual(parser.parse_args([u'-f', u'$bar']),
({'format': u'$bar'}, []))
self.assertEqual(parser.parse_args([u'--format', u'$baz']),
({'format': u'$baz'}, []))
self.assertEqual(config['format_item'].as_str(), u'$baz')
self.assertEqual(config['format_album'].as_str(), u'$baz')
def test_format_option_with_target(self):
with self.assertRaises(KeyError):
ui.CommonOptionsParser().add_format_option(target='thingy')
parser = ui.CommonOptionsParser()
parser.add_format_option(target='item')
config['format_item'].set('$item')
config['format_album'].set('$album')
self.assertEqual(parser.parse_args([u'-f', u'$bar']),
({'format': u'$bar'}, []))
self.assertEqual(config['format_item'].as_str(), u'$bar')
self.assertEqual(config['format_album'].as_str(), u'$album')
def test_format_option_with_album(self):
parser = ui.CommonOptionsParser()
parser.add_album_option()
parser.add_format_option()
config['format_item'].set('$item')
config['format_album'].set('$album')
parser.parse_args([u'-f', u'$bar'])
self.assertEqual(config['format_item'].as_str(), u'$bar')
self.assertEqual(config['format_album'].as_str(), u'$album')
parser.parse_args([u'-a', u'-f', u'$foo'])
self.assertEqual(config['format_item'].as_str(), u'$bar')
self.assertEqual(config['format_album'].as_str(), u'$foo')
parser.parse_args([u'-f', u'$foo2', u'-a'])
self.assertEqual(config['format_album'].as_str(), u'$foo2')
def test_add_all_common_options(self):
parser = ui.CommonOptionsParser()
parser.add_all_common_options()
self.assertEqual(parser.parse_args([]),
({'album': None, 'path': None, 'format': None}, []))
class EncodingTest(_common.TestCase):
"""Tests for the `terminal_encoding` config option and our
`_in_encoding` and `_out_encoding` utility functions.
"""
def out_encoding_overridden(self):
config['terminal_encoding'] = 'fake_encoding'
self.assertEqual(ui._out_encoding(), 'fake_encoding')
def in_encoding_overridden(self):
config['terminal_encoding'] = 'fake_encoding'
self.assertEqual(ui._in_encoding(), 'fake_encoding')
def out_encoding_default_utf8(self):
with patch('sys.stdout') as stdout:
stdout.encoding = None
self.assertEqual(ui._out_encoding(), 'utf-8')
def in_encoding_default_utf8(self):
with patch('sys.stdin') as stdin:
stdin.encoding = None
self.assertEqual(ui._in_encoding(), 'utf-8')
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/test_convert.py 0000644 0000765 0000024 00000023517 13120341455 017646 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Thomas Scholtes.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
from __future__ import division, absolute_import, print_function
import sys
import re
import os.path
import unittest
from test import _common
from test import helper
from test.helper import control_stdin, capture_log
from beets.mediafile import MediaFile
from beets import util
def shell_quote(text):
if sys.version_info[0] < 3:
import pipes
return pipes.quote(text)
else:
import shlex
return shlex.quote(text)
class TestHelper(helper.TestHelper):
def tagged_copy_cmd(self, tag):
"""Return a conversion command that copies files and appends
`tag` to the copy.
"""
if re.search('[^a-zA-Z0-9]', tag):
raise ValueError(u"tag '{0}' must only contain letters and digits"
.format(tag))
# A Python script that copies the file and appends a tag.
stub = os.path.join(_common.RSRC, b'convert_stub.py').decode('utf-8')
return u"{} {} $source $dest {}".format(shell_quote(sys.executable),
shell_quote(stub), tag)
def assertFileTag(self, path, tag): # noqa
"""Assert that the path is a file and the files content ends with `tag`.
"""
display_tag = tag
tag = tag.encode('utf-8')
self.assertTrue(os.path.isfile(path),
u'{0} is not a file'.format(
util.displayable_path(path)))
with open(path, 'rb') as f:
f.seek(-len(display_tag), os.SEEK_END)
self.assertEqual(f.read(), tag,
u'{0} is not tagged with {1}'
.format(
util.displayable_path(path),
display_tag))
def assertNoFileTag(self, path, tag): # noqa
"""Assert that the path is a file and the files content does not
end with `tag`.
"""
display_tag = tag
tag = tag.encode('utf-8')
self.assertTrue(os.path.isfile(path),
u'{0} is not a file'.format(
util.displayable_path(path)))
with open(path, 'rb') as f:
f.seek(-len(tag), os.SEEK_END)
self.assertNotEqual(f.read(), tag,
u'{0} is unexpectedly tagged with {1}'
.format(
util.displayable_path(path),
display_tag))
@_common.slow_test()
class ImportConvertTest(unittest.TestCase, TestHelper):
def setUp(self):
self.setup_beets(disk=True) # Converter is threaded
self.importer = self.create_importer()
self.load_plugins('convert')
self.config['convert'] = {
'dest': os.path.join(self.temp_dir, b'convert'),
'command': self.tagged_copy_cmd('convert'),
# Enforce running convert
'max_bitrate': 1,
'auto': True,
'quiet': False,
}
def tearDown(self):
self.unload_plugins()
self.teardown_beets()
def test_import_converted(self):
self.importer.run()
item = self.lib.items().get()
self.assertFileTag(item.path, 'convert')
def test_import_original_on_convert_error(self):
# `false` exits with non-zero code
self.config['convert']['command'] = u'false'
self.importer.run()
item = self.lib.items().get()
self.assertIsNotNone(item)
self.assertTrue(os.path.isfile(item.path))
class ConvertCommand(object):
"""A mixin providing a utility method to run the `convert`command
in tests.
"""
def run_convert_path(self, path, *args):
"""Run the `convert` command on a given path."""
# The path is currently a filesystem bytestring. Convert it to
# an argument bytestring.
path = path.decode(util._fsencoding()).encode(util.arg_encoding())
args = args + (b'path:' + path,)
return self.run_command('convert', *args)
def run_convert(self, *args):
"""Run the `convert` command on `self.item`."""
return self.run_convert_path(self.item.path, *args)
@_common.slow_test()
class ConvertCliTest(unittest.TestCase, TestHelper, ConvertCommand):
def setUp(self):
self.setup_beets(disk=True) # Converter is threaded
self.album = self.add_album_fixture(ext='ogg')
self.item = self.album.items()[0]
self.load_plugins('convert')
self.convert_dest = util.bytestring_path(
os.path.join(self.temp_dir, b'convert_dest')
)
self.config['convert'] = {
'dest': self.convert_dest,
'paths': {'default': 'converted'},
'format': 'mp3',
'formats': {
'mp3': self.tagged_copy_cmd('mp3'),
'opus': {
'command': self.tagged_copy_cmd('opus'),
'extension': 'ops',
}
}
}
def tearDown(self):
self.unload_plugins()
self.teardown_beets()
def test_convert(self):
with control_stdin('y'):
self.run_convert()
converted = os.path.join(self.convert_dest, b'converted.mp3')
self.assertFileTag(converted, 'mp3')
def test_convert_with_auto_confirmation(self):
self.run_convert('--yes')
converted = os.path.join(self.convert_dest, b'converted.mp3')
self.assertFileTag(converted, 'mp3')
def test_reject_confirmation(self):
with control_stdin('n'):
self.run_convert()
converted = os.path.join(self.convert_dest, b'converted.mp3')
self.assertFalse(os.path.isfile(converted))
def test_convert_keep_new(self):
self.assertEqual(os.path.splitext(self.item.path)[1], b'.ogg')
with control_stdin('y'):
self.run_convert('--keep-new')
self.item.load()
self.assertEqual(os.path.splitext(self.item.path)[1], b'.mp3')
def test_format_option(self):
with control_stdin('y'):
self.run_convert('--format', 'opus')
converted = os.path.join(self.convert_dest, b'converted.ops')
self.assertFileTag(converted, 'opus')
def test_embed_album_art(self):
self.config['convert']['embed'] = True
image_path = os.path.join(_common.RSRC, b'image-2x3.jpg')
self.album.artpath = image_path
self.album.store()
with open(os.path.join(image_path), 'rb') as f:
image_data = f.read()
with control_stdin('y'):
self.run_convert()
converted = os.path.join(self.convert_dest, b'converted.mp3')
mediafile = MediaFile(converted)
self.assertEqual(mediafile.images[0].data, image_data)
def test_skip_existing(self):
converted = os.path.join(self.convert_dest, b'converted.mp3')
self.touch(converted, content='XXX')
self.run_convert('--yes')
with open(converted, 'r') as f:
self.assertEqual(f.read(), 'XXX')
def test_pretend(self):
self.run_convert('--pretend')
converted = os.path.join(self.convert_dest, b'converted.mp3')
self.assertFalse(os.path.exists(converted))
def test_empty_query(self):
with capture_log('beets.convert') as logs:
self.run_convert('An impossible query')
self.assertEqual(logs[0], u'convert: Empty query result.')
@_common.slow_test()
class NeverConvertLossyFilesTest(unittest.TestCase, TestHelper,
ConvertCommand):
"""Test the effect of the `never_convert_lossy_files` option.
"""
def setUp(self):
self.setup_beets(disk=True) # Converter is threaded
self.load_plugins('convert')
self.convert_dest = os.path.join(self.temp_dir, b'convert_dest')
self.config['convert'] = {
'dest': self.convert_dest,
'paths': {'default': 'converted'},
'never_convert_lossy_files': True,
'format': 'mp3',
'formats': {
'mp3': self.tagged_copy_cmd('mp3'),
}
}
def tearDown(self):
self.unload_plugins()
self.teardown_beets()
def test_transcode_from_lossles(self):
[item] = self.add_item_fixtures(ext='flac')
with control_stdin('y'):
self.run_convert_path(item.path)
converted = os.path.join(self.convert_dest, b'converted.mp3')
self.assertFileTag(converted, 'mp3')
def test_transcode_from_lossy(self):
self.config['convert']['never_convert_lossy_files'] = False
[item] = self.add_item_fixtures(ext='ogg')
with control_stdin('y'):
self.run_convert_path(item.path)
converted = os.path.join(self.convert_dest, b'converted.mp3')
self.assertFileTag(converted, 'mp3')
def test_transcode_from_lossy_prevented(self):
[item] = self.add_item_fixtures(ext='ogg')
with control_stdin('y'):
self.run_convert_path(item.path)
converted = os.path.join(self.convert_dest, b'converted.ogg')
self.assertNoFileTag(converted, 'mp3')
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/test_ui_init.py 0000644 0000765 0000024 00000006773 13025125203 017626 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Test module for file ui/__init__.py
"""
from __future__ import division, absolute_import, print_function
import unittest
from test import _common
from beets import ui
class InputMethodsTest(_common.TestCase):
def setUp(self):
super(InputMethodsTest, self).setUp()
self.io.install()
def _print_helper(self, s):
print(s)
def _print_helper2(self, s, prefix):
print(prefix, s)
def test_input_select_objects(self):
full_items = ['1', '2', '3', '4', '5']
# Test no
self.io.addinput('n')
items = ui.input_select_objects(
"Prompt", full_items, self._print_helper)
self.assertEqual(items, [])
# Test yes
self.io.addinput('y')
items = ui.input_select_objects(
"Prompt", full_items, self._print_helper)
self.assertEqual(items, full_items)
# Test selective 1
self.io.addinput('s')
self.io.addinput('n')
self.io.addinput('y')
self.io.addinput('n')
self.io.addinput('y')
self.io.addinput('n')
items = ui.input_select_objects(
"Prompt", full_items, self._print_helper)
self.assertEqual(items, ['2', '4'])
# Test selective 2
self.io.addinput('s')
self.io.addinput('y')
self.io.addinput('y')
self.io.addinput('n')
self.io.addinput('y')
self.io.addinput('n')
items = ui.input_select_objects(
"Prompt", full_items,
lambda s: self._print_helper2(s, "Prefix"))
self.assertEqual(items, ['1', '2', '4'])
class InitTest(_common.LibTestCase):
def setUp(self):
super(InitTest, self).setUp()
def test_human_bytes(self):
tests = [
(0, '0.0 B'),
(30, '30.0 B'),
(pow(2, 10), '1.0 KiB'),
(pow(2, 20), '1.0 MiB'),
(pow(2, 30), '1.0 GiB'),
(pow(2, 40), '1.0 TiB'),
(pow(2, 50), '1.0 PiB'),
(pow(2, 60), '1.0 EiB'),
(pow(2, 70), '1.0 ZiB'),
(pow(2, 80), '1.0 YiB'),
(pow(2, 90), '1.0 HiB'),
(pow(2, 100), 'big'),
]
for i, h in tests:
self.assertEqual(h, ui.human_bytes(i))
def test_human_seconds(self):
tests = [
(0, '0.0 seconds'),
(30, '30.0 seconds'),
(60, '1.0 minutes'),
(90, '1.5 minutes'),
(125, '2.1 minutes'),
(3600, '1.0 hours'),
(86400, '1.0 days'),
(604800, '1.0 weeks'),
(31449600, '1.0 years'),
(314496000, '1.0 decades'),
]
for i, h in tests:
self.assertEqual(h, ui.human_seconds(i))
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/test_util.py 0000644 0000765 0000024 00000015647 13025125203 017143 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Tests for base utils from the beets.util package.
"""
from __future__ import division, absolute_import, print_function
import sys
import re
import os
import subprocess
import unittest
from mock import patch, Mock
from test import _common
from beets import util
import six
class UtilTest(unittest.TestCase):
def test_open_anything(self):
with _common.system_mock('Windows'):
self.assertEqual(util.open_anything(), 'start')
with _common.system_mock('Darwin'):
self.assertEqual(util.open_anything(), 'open')
with _common.system_mock('Tagada'):
self.assertEqual(util.open_anything(), 'xdg-open')
@patch('os.execlp')
@patch('beets.util.open_anything')
def test_interactive_open(self, mock_open, mock_execlp):
mock_open.return_value = u'tagada'
util.interactive_open(['foo'], util.open_anything())
mock_execlp.assert_called_once_with(u'tagada', u'tagada', u'foo')
mock_execlp.reset_mock()
util.interactive_open(['foo'], u'bar')
mock_execlp.assert_called_once_with(u'bar', u'bar', u'foo')
def test_sanitize_unix_replaces_leading_dot(self):
with _common.platform_posix():
p = util.sanitize_path(u'one/.two/three')
self.assertFalse(u'.' in p)
def test_sanitize_windows_replaces_trailing_dot(self):
with _common.platform_windows():
p = util.sanitize_path(u'one/two./three')
self.assertFalse(u'.' in p)
def test_sanitize_windows_replaces_illegal_chars(self):
with _common.platform_windows():
p = util.sanitize_path(u':*?"<>|')
self.assertFalse(u':' in p)
self.assertFalse(u'*' in p)
self.assertFalse(u'?' in p)
self.assertFalse(u'"' in p)
self.assertFalse(u'<' in p)
self.assertFalse(u'>' in p)
self.assertFalse(u'|' in p)
def test_sanitize_windows_replaces_trailing_space(self):
with _common.platform_windows():
p = util.sanitize_path(u'one/two /three')
self.assertFalse(u' ' in p)
def test_sanitize_path_works_on_empty_string(self):
with _common.platform_posix():
p = util.sanitize_path(u'')
self.assertEqual(p, u'')
def test_sanitize_with_custom_replace_overrides_built_in_sub(self):
with _common.platform_posix():
p = util.sanitize_path(u'a/.?/b', [
(re.compile(r'foo'), u'bar'),
])
self.assertEqual(p, u'a/.?/b')
def test_sanitize_with_custom_replace_adds_replacements(self):
with _common.platform_posix():
p = util.sanitize_path(u'foo/bar', [
(re.compile(r'foo'), u'bar'),
])
self.assertEqual(p, u'bar/bar')
@unittest.skip(u'unimplemented: #359')
def test_sanitize_empty_component(self):
with _common.platform_posix():
p = util.sanitize_path(u'foo//bar', [
(re.compile(r'^$'), u'_'),
])
self.assertEqual(p, u'foo/_/bar')
@unittest.skipIf(six.PY2, 'surrogateescape error handler not available'
'on Python 2')
def test_convert_command_args_keeps_undecodeable_bytes(self):
arg = b'\x82' # non-ascii bytes
cmd_args = util.convert_command_args([arg])
self.assertEqual(cmd_args[0],
arg.decode(util.arg_encoding(), 'surrogateescape'))
@patch('beets.util.subprocess.Popen')
def test_command_output(self, mock_popen):
def popen_fail(*args, **kwargs):
m = Mock(returncode=1)
m.communicate.return_value = u'foo', u'bar'
return m
mock_popen.side_effect = popen_fail
with self.assertRaises(subprocess.CalledProcessError) as exc_context:
util.command_output(['taga', '\xc3\xa9'])
self.assertEqual(exc_context.exception.returncode, 1)
self.assertEqual(exc_context.exception.cmd, 'taga \xc3\xa9')
class PathConversionTest(_common.TestCase):
def test_syspath_windows_format(self):
with _common.platform_windows():
path = os.path.join(u'a', u'b', u'c')
outpath = util.syspath(path)
self.assertTrue(isinstance(outpath, six.text_type))
self.assertTrue(outpath.startswith(u'\\\\?\\'))
def test_syspath_windows_format_unc_path(self):
# The \\?\ prefix on Windows behaves differently with UNC
# (network share) paths.
path = '\\\\server\\share\\file.mp3'
with _common.platform_windows():
outpath = util.syspath(path)
self.assertTrue(isinstance(outpath, six.text_type))
self.assertEqual(outpath, u'\\\\?\\UNC\\server\\share\\file.mp3')
def test_syspath_posix_unchanged(self):
with _common.platform_posix():
path = os.path.join(u'a', u'b', u'c')
outpath = util.syspath(path)
self.assertEqual(path, outpath)
def _windows_bytestring_path(self, path):
old_gfse = sys.getfilesystemencoding
sys.getfilesystemencoding = lambda: 'mbcs'
try:
with _common.platform_windows():
return util.bytestring_path(path)
finally:
sys.getfilesystemencoding = old_gfse
def test_bytestring_path_windows_encodes_utf8(self):
path = u'caf\xe9'
outpath = self._windows_bytestring_path(path)
self.assertEqual(path, outpath.decode('utf-8'))
def test_bytesting_path_windows_removes_magic_prefix(self):
path = u'\\\\?\\C:\\caf\xe9'
outpath = self._windows_bytestring_path(path)
self.assertEqual(outpath, u'C:\\caf\xe9'.encode('utf-8'))
class PathTruncationTest(_common.TestCase):
def test_truncate_bytestring(self):
with _common.platform_posix():
p = util.truncate_path(b'abcde/fgh', 4)
self.assertEqual(p, b'abcd/fgh')
def test_truncate_unicode(self):
with _common.platform_posix():
p = util.truncate_path(u'abcde/fgh', 4)
self.assertEqual(p, u'abcd/fgh')
def test_truncate_preserves_extension(self):
with _common.platform_posix():
p = util.truncate_path(u'abcde/fgh.ext', 5)
self.assertEqual(p, u'abcde/f.ext')
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/test_sort.py 0000644 0000765 0000024 00000047647 13025125203 017162 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Various tests for querying the library database.
"""
from __future__ import division, absolute_import, print_function
import unittest
from test import _common
import beets.library
from beets import dbcore
from beets import config
# A test case class providing a library with some dummy data and some
# assertions involving that data.
class DummyDataTestCase(_common.TestCase):
def setUp(self):
super(DummyDataTestCase, self).setUp()
self.lib = beets.library.Library(':memory:')
albums = [_common.album() for _ in range(3)]
albums[0].album = u"Album A"
albums[0].genre = u"Rock"
albums[0].year = 2001
albums[0].flex1 = u"Flex1-1"
albums[0].flex2 = u"Flex2-A"
albums[0].albumartist = u"Foo"
albums[0].albumartist_sort = None
albums[1].album = u"Album B"
albums[1].genre = u"Rock"
albums[1].year = 2001
albums[1].flex1 = u"Flex1-2"
albums[1].flex2 = u"Flex2-A"
albums[1].albumartist = u"Bar"
albums[1].albumartist_sort = None
albums[2].album = u"Album C"
albums[2].genre = u"Jazz"
albums[2].year = 2005
albums[2].flex1 = u"Flex1-1"
albums[2].flex2 = u"Flex2-B"
albums[2].albumartist = u"Baz"
albums[2].albumartist_sort = None
for album in albums:
self.lib.add(album)
items = [_common.item() for _ in range(4)]
items[0].title = u'Foo bar'
items[0].artist = u'One'
items[0].album = u'Baz'
items[0].year = 2001
items[0].comp = True
items[0].flex1 = u"Flex1-0"
items[0].flex2 = u"Flex2-A"
items[0].album_id = albums[0].id
items[0].artist_sort = None
items[0].path = "/path0.mp3"
items[0].track = 1
items[1].title = u'Baz qux'
items[1].artist = u'Two'
items[1].album = u'Baz'
items[1].year = 2002
items[1].comp = True
items[1].flex1 = u"Flex1-1"
items[1].flex2 = u"Flex2-A"
items[1].album_id = albums[0].id
items[1].artist_sort = None
items[1].path = "/patH1.mp3"
items[1].track = 2
items[2].title = u'Beets 4 eva'
items[2].artist = u'Three'
items[2].album = u'Foo'
items[2].year = 2003
items[2].comp = False
items[2].flex1 = u"Flex1-2"
items[2].flex2 = u"Flex1-B"
items[2].album_id = albums[1].id
items[2].artist_sort = None
items[2].path = "/paTH2.mp3"
items[2].track = 3
items[3].title = u'Beets 4 eva'
items[3].artist = u'Three'
items[3].album = u'Foo2'
items[3].year = 2004
items[3].comp = False
items[3].flex1 = u"Flex1-2"
items[3].flex2 = u"Flex1-C"
items[3].album_id = albums[2].id
items[3].artist_sort = None
items[3].path = "/PATH3.mp3"
items[3].track = 4
for item in items:
self.lib.add(item)
class SortFixedFieldTest(DummyDataTestCase):
def test_sort_asc(self):
q = u''
sort = dbcore.query.FixedFieldSort(u"year", True)
results = self.lib.items(q, sort)
self.assertLessEqual(results[0]['year'], results[1]['year'])
self.assertEqual(results[0]['year'], 2001)
# same thing with query string
q = u'year+'
results2 = self.lib.items(q)
for r1, r2 in zip(results, results2):
self.assertEqual(r1.id, r2.id)
def test_sort_desc(self):
q = u''
sort = dbcore.query.FixedFieldSort(u"year", False)
results = self.lib.items(q, sort)
self.assertGreaterEqual(results[0]['year'], results[1]['year'])
self.assertEqual(results[0]['year'], 2004)
# same thing with query string
q = u'year-'
results2 = self.lib.items(q)
for r1, r2 in zip(results, results2):
self.assertEqual(r1.id, r2.id)
def test_sort_two_field_asc(self):
q = u''
s1 = dbcore.query.FixedFieldSort(u"album", True)
s2 = dbcore.query.FixedFieldSort(u"year", True)
sort = dbcore.query.MultipleSort()
sort.add_sort(s1)
sort.add_sort(s2)
results = self.lib.items(q, sort)
self.assertLessEqual(results[0]['album'], results[1]['album'])
self.assertLessEqual(results[1]['album'], results[2]['album'])
self.assertEqual(results[0]['album'], u'Baz')
self.assertEqual(results[1]['album'], u'Baz')
self.assertLessEqual(results[0]['year'], results[1]['year'])
# same thing with query string
q = u'album+ year+'
results2 = self.lib.items(q)
for r1, r2 in zip(results, results2):
self.assertEqual(r1.id, r2.id)
def test_sort_path_field(self):
q = u''
sort = dbcore.query.FixedFieldSort('path', True)
results = self.lib.items(q, sort)
self.assertEqual(results[0]['path'], b'/path0.mp3')
self.assertEqual(results[1]['path'], b'/patH1.mp3')
self.assertEqual(results[2]['path'], b'/paTH2.mp3')
self.assertEqual(results[3]['path'], b'/PATH3.mp3')
class SortFlexFieldTest(DummyDataTestCase):
def test_sort_asc(self):
q = u''
sort = dbcore.query.SlowFieldSort(u"flex1", True)
results = self.lib.items(q, sort)
self.assertLessEqual(results[0]['flex1'], results[1]['flex1'])
self.assertEqual(results[0]['flex1'], u'Flex1-0')
# same thing with query string
q = u'flex1+'
results2 = self.lib.items(q)
for r1, r2 in zip(results, results2):
self.assertEqual(r1.id, r2.id)
def test_sort_desc(self):
q = u''
sort = dbcore.query.SlowFieldSort(u"flex1", False)
results = self.lib.items(q, sort)
self.assertGreaterEqual(results[0]['flex1'], results[1]['flex1'])
self.assertGreaterEqual(results[1]['flex1'], results[2]['flex1'])
self.assertGreaterEqual(results[2]['flex1'], results[3]['flex1'])
self.assertEqual(results[0]['flex1'], u'Flex1-2')
# same thing with query string
q = u'flex1-'
results2 = self.lib.items(q)
for r1, r2 in zip(results, results2):
self.assertEqual(r1.id, r2.id)
def test_sort_two_field(self):
q = u''
s1 = dbcore.query.SlowFieldSort(u"flex2", False)
s2 = dbcore.query.SlowFieldSort(u"flex1", True)
sort = dbcore.query.MultipleSort()
sort.add_sort(s1)
sort.add_sort(s2)
results = self.lib.items(q, sort)
self.assertGreaterEqual(results[0]['flex2'], results[1]['flex2'])
self.assertGreaterEqual(results[1]['flex2'], results[2]['flex2'])
self.assertEqual(results[0]['flex2'], u'Flex2-A')
self.assertEqual(results[1]['flex2'], u'Flex2-A')
self.assertLessEqual(results[0]['flex1'], results[1]['flex1'])
# same thing with query string
q = u'flex2- flex1+'
results2 = self.lib.items(q)
for r1, r2 in zip(results, results2):
self.assertEqual(r1.id, r2.id)
class SortAlbumFixedFieldTest(DummyDataTestCase):
def test_sort_asc(self):
q = u''
sort = dbcore.query.FixedFieldSort(u"year", True)
results = self.lib.albums(q, sort)
self.assertLessEqual(results[0]['year'], results[1]['year'])
self.assertEqual(results[0]['year'], 2001)
# same thing with query string
q = u'year+'
results2 = self.lib.albums(q)
for r1, r2 in zip(results, results2):
self.assertEqual(r1.id, r2.id)
def test_sort_desc(self):
q = u''
sort = dbcore.query.FixedFieldSort(u"year", False)
results = self.lib.albums(q, sort)
self.assertGreaterEqual(results[0]['year'], results[1]['year'])
self.assertEqual(results[0]['year'], 2005)
# same thing with query string
q = u'year-'
results2 = self.lib.albums(q)
for r1, r2 in zip(results, results2):
self.assertEqual(r1.id, r2.id)
def test_sort_two_field_asc(self):
q = u''
s1 = dbcore.query.FixedFieldSort(u"genre", True)
s2 = dbcore.query.FixedFieldSort(u"album", True)
sort = dbcore.query.MultipleSort()
sort.add_sort(s1)
sort.add_sort(s2)
results = self.lib.albums(q, sort)
self.assertLessEqual(results[0]['genre'], results[1]['genre'])
self.assertLessEqual(results[1]['genre'], results[2]['genre'])
self.assertEqual(results[1]['genre'], u'Rock')
self.assertEqual(results[2]['genre'], u'Rock')
self.assertLessEqual(results[1]['album'], results[2]['album'])
# same thing with query string
q = u'genre+ album+'
results2 = self.lib.albums(q)
for r1, r2 in zip(results, results2):
self.assertEqual(r1.id, r2.id)
class SortAlbumFlexFieldTest(DummyDataTestCase):
def test_sort_asc(self):
q = u''
sort = dbcore.query.SlowFieldSort(u"flex1", True)
results = self.lib.albums(q, sort)
self.assertLessEqual(results[0]['flex1'], results[1]['flex1'])
self.assertLessEqual(results[1]['flex1'], results[2]['flex1'])
# same thing with query string
q = u'flex1+'
results2 = self.lib.albums(q)
for r1, r2 in zip(results, results2):
self.assertEqual(r1.id, r2.id)
def test_sort_desc(self):
q = u''
sort = dbcore.query.SlowFieldSort(u"flex1", False)
results = self.lib.albums(q, sort)
self.assertGreaterEqual(results[0]['flex1'], results[1]['flex1'])
self.assertGreaterEqual(results[1]['flex1'], results[2]['flex1'])
# same thing with query string
q = u'flex1-'
results2 = self.lib.albums(q)
for r1, r2 in zip(results, results2):
self.assertEqual(r1.id, r2.id)
def test_sort_two_field_asc(self):
q = u''
s1 = dbcore.query.SlowFieldSort(u"flex2", True)
s2 = dbcore.query.SlowFieldSort(u"flex1", True)
sort = dbcore.query.MultipleSort()
sort.add_sort(s1)
sort.add_sort(s2)
results = self.lib.albums(q, sort)
self.assertLessEqual(results[0]['flex2'], results[1]['flex2'])
self.assertLessEqual(results[1]['flex2'], results[2]['flex2'])
self.assertEqual(results[0]['flex2'], u'Flex2-A')
self.assertEqual(results[1]['flex2'], u'Flex2-A')
self.assertLessEqual(results[0]['flex1'], results[1]['flex1'])
# same thing with query string
q = u'flex2+ flex1+'
results2 = self.lib.albums(q)
for r1, r2 in zip(results, results2):
self.assertEqual(r1.id, r2.id)
class SortAlbumComputedFieldTest(DummyDataTestCase):
def test_sort_asc(self):
q = u''
sort = dbcore.query.SlowFieldSort(u"path", True)
results = self.lib.albums(q, sort)
self.assertLessEqual(results[0]['path'], results[1]['path'])
self.assertLessEqual(results[1]['path'], results[2]['path'])
# same thing with query string
q = u'path+'
results2 = self.lib.albums(q)
for r1, r2 in zip(results, results2):
self.assertEqual(r1.id, r2.id)
def test_sort_desc(self):
q = u''
sort = dbcore.query.SlowFieldSort(u"path", False)
results = self.lib.albums(q, sort)
self.assertGreaterEqual(results[0]['path'], results[1]['path'])
self.assertGreaterEqual(results[1]['path'], results[2]['path'])
# same thing with query string
q = u'path-'
results2 = self.lib.albums(q)
for r1, r2 in zip(results, results2):
self.assertEqual(r1.id, r2.id)
class SortCombinedFieldTest(DummyDataTestCase):
def test_computed_first(self):
q = u''
s1 = dbcore.query.SlowFieldSort(u"path", True)
s2 = dbcore.query.FixedFieldSort(u"year", True)
sort = dbcore.query.MultipleSort()
sort.add_sort(s1)
sort.add_sort(s2)
results = self.lib.albums(q, sort)
self.assertLessEqual(results[0]['path'], results[1]['path'])
self.assertLessEqual(results[1]['path'], results[2]['path'])
q = u'path+ year+'
results2 = self.lib.albums(q)
for r1, r2 in zip(results, results2):
self.assertEqual(r1.id, r2.id)
def test_computed_second(self):
q = u''
s1 = dbcore.query.FixedFieldSort(u"year", True)
s2 = dbcore.query.SlowFieldSort(u"path", True)
sort = dbcore.query.MultipleSort()
sort.add_sort(s1)
sort.add_sort(s2)
results = self.lib.albums(q, sort)
self.assertLessEqual(results[0]['year'], results[1]['year'])
self.assertLessEqual(results[1]['year'], results[2]['year'])
self.assertLessEqual(results[0]['path'], results[1]['path'])
q = u'year+ path+'
results2 = self.lib.albums(q)
for r1, r2 in zip(results, results2):
self.assertEqual(r1.id, r2.id)
class ConfigSortTest(DummyDataTestCase):
def test_default_sort_item(self):
results = list(self.lib.items())
self.assertLess(results[0].artist, results[1].artist)
def test_config_opposite_sort_item(self):
config['sort_item'] = 'artist-'
results = list(self.lib.items())
self.assertGreater(results[0].artist, results[1].artist)
def test_default_sort_album(self):
results = list(self.lib.albums())
self.assertLess(results[0].albumartist, results[1].albumartist)
def test_config_opposite_sort_album(self):
config['sort_album'] = 'albumartist-'
results = list(self.lib.albums())
self.assertGreater(results[0].albumartist, results[1].albumartist)
class CaseSensitivityTest(DummyDataTestCase, _common.TestCase):
"""If case_insensitive is false, lower-case values should be placed
after all upper-case values. E.g., `Foo Qux bar`
"""
def setUp(self):
super(CaseSensitivityTest, self).setUp()
album = _common.album()
album.album = u"album"
album.genre = u"alternative"
album.year = u"2001"
album.flex1 = u"flex1"
album.flex2 = u"flex2-A"
album.albumartist = u"bar"
album.albumartist_sort = None
self.lib.add(album)
item = _common.item()
item.title = u'another'
item.artist = u'lowercase'
item.album = u'album'
item.year = 2001
item.comp = True
item.flex1 = u"flex1"
item.flex2 = u"flex2-A"
item.album_id = album.id
item.artist_sort = None
item.track = 10
self.lib.add(item)
self.new_album = album
self.new_item = item
def tearDown(self):
self.new_item.remove(delete=True)
self.new_album.remove(delete=True)
super(CaseSensitivityTest, self).tearDown()
def test_smart_artist_case_insensitive(self):
config['sort_case_insensitive'] = True
q = u'artist+'
results = list(self.lib.items(q))
self.assertEqual(results[0].artist, u'lowercase')
self.assertEqual(results[1].artist, u'One')
def test_smart_artist_case_sensitive(self):
config['sort_case_insensitive'] = False
q = u'artist+'
results = list(self.lib.items(q))
self.assertEqual(results[0].artist, u'One')
self.assertEqual(results[-1].artist, u'lowercase')
def test_fixed_field_case_insensitive(self):
config['sort_case_insensitive'] = True
q = u'album+'
results = list(self.lib.albums(q))
self.assertEqual(results[0].album, u'album')
self.assertEqual(results[1].album, u'Album A')
def test_fixed_field_case_sensitive(self):
config['sort_case_insensitive'] = False
q = u'album+'
results = list(self.lib.albums(q))
self.assertEqual(results[0].album, u'Album A')
self.assertEqual(results[-1].album, u'album')
def test_flex_field_case_insensitive(self):
config['sort_case_insensitive'] = True
q = u'flex1+'
results = list(self.lib.items(q))
self.assertEqual(results[0].flex1, u'flex1')
self.assertEqual(results[1].flex1, u'Flex1-0')
def test_flex_field_case_sensitive(self):
config['sort_case_insensitive'] = False
q = u'flex1+'
results = list(self.lib.items(q))
self.assertEqual(results[0].flex1, u'Flex1-0')
self.assertEqual(results[-1].flex1, u'flex1')
def test_case_sensitive_only_affects_text(self):
config['sort_case_insensitive'] = True
q = u'track+'
results = list(self.lib.items(q))
# If the numerical values were sorted as strings,
# then ['1', '10', '2'] would be valid.
print([r.track for r in results])
self.assertEqual(results[0].track, 1)
self.assertEqual(results[1].track, 2)
self.assertEqual(results[-1].track, 10)
class NonExistingFieldTest(DummyDataTestCase):
"""Test sorting by non-existing fields"""
def test_non_existing_fields_not_fail(self):
qs = [u'foo+', u'foo-', u'--', u'-+', u'+-',
u'++', u'-foo-', u'-foo+', u'---']
q0 = u'foo+'
results0 = list(self.lib.items(q0))
for q1 in qs:
results1 = list(self.lib.items(q1))
for r1, r2 in zip(results0, results1):
self.assertEqual(r1.id, r2.id)
def test_combined_non_existing_field_asc(self):
all_results = list(self.lib.items(u'id+'))
q = u'foo+ id+'
results = list(self.lib.items(q))
self.assertEqual(len(all_results), len(results))
for r1, r2 in zip(all_results, results):
self.assertEqual(r1.id, r2.id)
def test_combined_non_existing_field_desc(self):
all_results = list(self.lib.items(u'id+'))
q = u'foo- id+'
results = list(self.lib.items(q))
self.assertEqual(len(all_results), len(results))
for r1, r2 in zip(all_results, results):
self.assertEqual(r1.id, r2.id)
def test_field_present_in_some_items(self):
"""Test ordering by a field not present on all items."""
# append 'foo' to two to items (1,2)
items = self.lib.items(u'id+')
ids = [i.id for i in items]
items[1].foo = u'bar1'
items[2].foo = u'bar2'
items[1].store()
items[2].store()
results_asc = list(self.lib.items(u'foo+ id+'))
self.assertEqual([i.id for i in results_asc],
# items without field first
[ids[0], ids[3], ids[1], ids[2]])
results_desc = list(self.lib.items(u'foo- id+'))
self.assertEqual([i.id for i in results_desc],
# items without field last
[ids[2], ids[1], ids[0], ids[3]])
def test_negation_interaction(self):
"""Test the handling of negation and sorting together.
If a string ends with a sorting suffix, it takes precedence over the
NotQuery parsing.
"""
query, sort = beets.library.parse_query_string(u'-bar+',
beets.library.Item)
self.assertEqual(len(query.subqueries), 1)
self.assertTrue(isinstance(query.subqueries[0],
dbcore.query.TrueQuery))
self.assertTrue(isinstance(sort, dbcore.query.SlowFieldSort))
self.assertEqual(sort.field, u'-bar')
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/test_plexupdate.py 0000644 0000765 0000024 00000011554 13025125203 020332 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
from __future__ import division, absolute_import, print_function
from test.helper import TestHelper
from beetsplug.plexupdate import get_music_section, update_plex
import unittest
import responses
class PlexUpdateTest(unittest.TestCase, TestHelper):
def add_response_get_music_section(self, section_name='Music'):
"""Create response for mocking the get_music_section function.
"""
escaped_section_name = section_name.replace('"', '\\"')
body = (
''
''
''
''
''
''
''
''
''
''
''
'')
status = 200
content_type = 'text/xml;charset=utf-8'
responses.add(responses.GET,
'http://localhost:32400/library/sections',
body=body,
status=status,
content_type=content_type)
def add_response_update_plex(self):
"""Create response for mocking the update_plex function.
"""
body = ''
status = 200
content_type = 'text/html'
responses.add(responses.GET,
'http://localhost:32400/library/sections/2/refresh',
body=body,
status=status,
content_type=content_type)
def setUp(self):
self.setup_beets()
self.load_plugins('plexupdate')
self.config['plex'] = {
u'host': u'localhost',
u'port': 32400}
def tearDown(self):
self.teardown_beets()
self.unload_plugins()
@responses.activate
def test_get_music_section(self):
# Adding response.
self.add_response_get_music_section()
# Test if section key is "2" out of the mocking data.
self.assertEqual(get_music_section(
self.config['plex']['host'],
self.config['plex']['port'],
self.config['plex']['token'],
self.config['plex']['library_name'].get()), '2')
@responses.activate
def test_get_named_music_section(self):
# Adding response.
self.add_response_get_music_section('My Music Library')
self.assertEqual(get_music_section(
self.config['plex']['host'],
self.config['plex']['port'],
self.config['plex']['token'],
'My Music Library'), '2')
@responses.activate
def test_update_plex(self):
# Adding responses.
self.add_response_get_music_section()
self.add_response_update_plex()
# Testing status code of the mocking request.
self.assertEqual(update_plex(
self.config['plex']['host'],
self.config['plex']['port'],
self.config['plex']['token'],
self.config['plex']['library_name'].get()).status_code, 200)
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/test_ipfs.py 0000644 0000765 0000024 00000006424 13122501062 017117 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
# This file is part of beets.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
from __future__ import division, absolute_import, print_function
from mock import patch, Mock
from beets import library
from beets.util import bytestring_path, _fsencoding
from beetsplug.ipfs import IPFSPlugin
import unittest
import os
from test import _common
from test.helper import TestHelper
@patch('beets.util.command_output', Mock())
class IPFSPluginTest(unittest.TestCase, TestHelper):
def setUp(self):
self.setup_beets()
self.load_plugins('ipfs')
self.lib = library.Library(":memory:")
def tearDown(self):
self.unload_plugins()
self.teardown_beets()
def test_stored_hashes(self):
test_album = self.mk_test_album()
ipfs = IPFSPlugin()
added_albums = ipfs.ipfs_added_albums(self.lib, self.lib.path)
added_album = added_albums.get_album(1)
self.assertEqual(added_album.ipfs, test_album.ipfs)
found = False
want_item = test_album.items()[2]
for check_item in added_album.items():
try:
if check_item.ipfs:
ipfs_item = os.path.basename(want_item.path).decode(
_fsencoding(),
)
want_path = '/ipfs/{0}/{1}'.format(test_album.ipfs,
ipfs_item)
want_path = bytestring_path(want_path)
self.assertEqual(check_item.path, want_path)
self.assertEqual(check_item.ipfs, want_item.ipfs)
self.assertEqual(check_item.title, want_item.title)
found = True
except AttributeError:
pass
self.assertTrue(found)
def mk_test_album(self):
items = [_common.item() for _ in range(3)]
items[0].title = 'foo bar'
items[0].artist = '1one'
items[0].album = 'baz'
items[0].year = 2001
items[0].comp = True
items[1].title = 'baz qux'
items[1].artist = '2two'
items[1].album = 'baz'
items[1].year = 2002
items[1].comp = True
items[2].title = 'beets 4 eva'
items[2].artist = '3three'
items[2].album = 'foo'
items[2].year = 2003
items[2].comp = False
items[2].ipfs = 'QmfM9ic5LJj7V6ecozFx1MkSoaaiq3PXfhJoFvyqzpLXSk'
for item in items:
self.lib.add(item)
album = self.lib.add_album(items)
album.ipfs = "QmfM9ic5LJj7V6ecozFx1MkSoaaiq3PXfhJoFvyqzpLXSf"
album.store()
return album
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/test_autotag.py 0000644 0000765 0000024 00000102734 13025125203 017624 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Tests for autotagging functionality.
"""
from __future__ import division, absolute_import, print_function
import re
import copy
import unittest
from test import _common
from beets import autotag
from beets.autotag import match
from beets.autotag.hooks import Distance, string_dist
from beets.library import Item
from beets.util import plurality
from beets.autotag import AlbumInfo, TrackInfo
from beets import config
class PluralityTest(_common.TestCase):
def test_plurality_consensus(self):
objs = [1, 1, 1, 1]
obj, freq = plurality(objs)
self.assertEqual(obj, 1)
self.assertEqual(freq, 4)
def test_plurality_near_consensus(self):
objs = [1, 1, 2, 1]
obj, freq = plurality(objs)
self.assertEqual(obj, 1)
self.assertEqual(freq, 3)
def test_plurality_conflict(self):
objs = [1, 1, 2, 2, 3]
obj, freq = plurality(objs)
self.assertTrue(obj in (1, 2))
self.assertEqual(freq, 2)
def test_plurality_empty_sequence_raises_error(self):
with self.assertRaises(ValueError):
plurality([])
def test_current_metadata_finds_pluralities(self):
items = [Item(artist='The Beetles', album='The White Album'),
Item(artist='The Beatles', album='The White Album'),
Item(artist='The Beatles', album='Teh White Album')]
likelies, consensus = match.current_metadata(items)
self.assertEqual(likelies['artist'], 'The Beatles')
self.assertEqual(likelies['album'], 'The White Album')
self.assertFalse(consensus['artist'])
def test_current_metadata_artist_consensus(self):
items = [Item(artist='The Beatles', album='The White Album'),
Item(artist='The Beatles', album='The White Album'),
Item(artist='The Beatles', album='Teh White Album')]
likelies, consensus = match.current_metadata(items)
self.assertEqual(likelies['artist'], 'The Beatles')
self.assertEqual(likelies['album'], 'The White Album')
self.assertTrue(consensus['artist'])
def test_albumartist_consensus(self):
items = [Item(artist='tartist1', album='album',
albumartist='aartist'),
Item(artist='tartist2', album='album',
albumartist='aartist'),
Item(artist='tartist3', album='album',
albumartist='aartist')]
likelies, consensus = match.current_metadata(items)
self.assertEqual(likelies['artist'], 'aartist')
self.assertFalse(consensus['artist'])
def test_current_metadata_likelies(self):
fields = ['artist', 'album', 'albumartist', 'year', 'disctotal',
'mb_albumid', 'label', 'catalognum', 'country', 'media',
'albumdisambig']
items = [Item(**dict((f, '%s_%s' % (f, i or 1)) for f in fields))
for i in range(5)]
likelies, _ = match.current_metadata(items)
for f in fields:
self.assertEqual(likelies[f], '%s_1' % f)
def _make_item(title, track, artist=u'some artist'):
return Item(title=title, track=track,
artist=artist, album=u'some album',
length=1,
mb_trackid='', mb_albumid='', mb_artistid='')
def _make_trackinfo():
return [
TrackInfo(u'one', None, u'some artist', length=1, index=1),
TrackInfo(u'two', None, u'some artist', length=1, index=2),
TrackInfo(u'three', None, u'some artist', length=1, index=3),
]
def _clear_weights():
"""Hack around the lazy descriptor used to cache weights for
Distance calculations.
"""
Distance.__dict__['_weights'].computed = False
class DistanceTest(_common.TestCase):
def tearDown(self):
super(DistanceTest, self).tearDown()
_clear_weights()
def test_add(self):
dist = Distance()
dist.add('add', 1.0)
self.assertEqual(dist._penalties, {'add': [1.0]})
def test_add_equality(self):
dist = Distance()
dist.add_equality('equality', 'ghi', ['abc', 'def', 'ghi'])
self.assertEqual(dist._penalties['equality'], [0.0])
dist.add_equality('equality', 'xyz', ['abc', 'def', 'ghi'])
self.assertEqual(dist._penalties['equality'], [0.0, 1.0])
dist.add_equality('equality', 'abc', re.compile(r'ABC', re.I))
self.assertEqual(dist._penalties['equality'], [0.0, 1.0, 0.0])
def test_add_expr(self):
dist = Distance()
dist.add_expr('expr', True)
self.assertEqual(dist._penalties['expr'], [1.0])
dist.add_expr('expr', False)
self.assertEqual(dist._penalties['expr'], [1.0, 0.0])
def test_add_number(self):
dist = Distance()
# Add a full penalty for each number of difference between two numbers.
dist.add_number('number', 1, 1)
self.assertEqual(dist._penalties['number'], [0.0])
dist.add_number('number', 1, 2)
self.assertEqual(dist._penalties['number'], [0.0, 1.0])
dist.add_number('number', 2, 1)
self.assertEqual(dist._penalties['number'], [0.0, 1.0, 1.0])
dist.add_number('number', -1, 2)
self.assertEqual(dist._penalties['number'], [0.0, 1.0, 1.0, 1.0,
1.0, 1.0])
def test_add_priority(self):
dist = Distance()
dist.add_priority('priority', 'abc', 'abc')
self.assertEqual(dist._penalties['priority'], [0.0])
dist.add_priority('priority', 'def', ['abc', 'def'])
self.assertEqual(dist._penalties['priority'], [0.0, 0.5])
dist.add_priority('priority', 'gh', ['ab', 'cd', 'ef',
re.compile('GH', re.I)])
self.assertEqual(dist._penalties['priority'], [0.0, 0.5, 0.75])
dist.add_priority('priority', 'xyz', ['abc', 'def'])
self.assertEqual(dist._penalties['priority'], [0.0, 0.5, 0.75,
1.0])
def test_add_ratio(self):
dist = Distance()
dist.add_ratio('ratio', 25, 100)
self.assertEqual(dist._penalties['ratio'], [0.25])
dist.add_ratio('ratio', 10, 5)
self.assertEqual(dist._penalties['ratio'], [0.25, 1.0])
dist.add_ratio('ratio', -5, 5)
self.assertEqual(dist._penalties['ratio'], [0.25, 1.0, 0.0])
dist.add_ratio('ratio', 5, 0)
self.assertEqual(dist._penalties['ratio'], [0.25, 1.0, 0.0, 0.0])
def test_add_string(self):
dist = Distance()
sdist = string_dist(u'abc', u'bcd')
dist.add_string('string', u'abc', u'bcd')
self.assertEqual(dist._penalties['string'], [sdist])
self.assertNotEqual(dist._penalties['string'], [0])
def test_add_string_none(self):
dist = Distance()
dist.add_string('string', None, 'string')
self.assertEqual(dist._penalties['string'], [1])
def test_add_string_both_none(self):
dist = Distance()
dist.add_string('string', None, None)
self.assertEqual(dist._penalties['string'], [0])
def test_distance(self):
config['match']['distance_weights']['album'] = 2.0
config['match']['distance_weights']['medium'] = 1.0
_clear_weights()
dist = Distance()
dist.add('album', 0.5)
dist.add('media', 0.25)
dist.add('media', 0.75)
self.assertEqual(dist.distance, 0.5)
# __getitem__()
self.assertEqual(dist['album'], 0.25)
self.assertEqual(dist['media'], 0.25)
def test_max_distance(self):
config['match']['distance_weights']['album'] = 3.0
config['match']['distance_weights']['medium'] = 1.0
_clear_weights()
dist = Distance()
dist.add('album', 0.5)
dist.add('medium', 0.0)
dist.add('medium', 0.0)
self.assertEqual(dist.max_distance, 5.0)
def test_operators(self):
config['match']['distance_weights']['source'] = 1.0
config['match']['distance_weights']['album'] = 2.0
config['match']['distance_weights']['medium'] = 1.0
_clear_weights()
dist = Distance()
dist.add('source', 0.0)
dist.add('album', 0.5)
dist.add('medium', 0.25)
dist.add('medium', 0.75)
self.assertEqual(len(dist), 2)
self.assertEqual(list(dist), [('album', 0.2), ('medium', 0.2)])
self.assertTrue(dist == 0.4)
self.assertTrue(dist < 1.0)
self.assertTrue(dist > 0.0)
self.assertEqual(dist - 0.4, 0.0)
self.assertEqual(0.4 - dist, 0.0)
self.assertEqual(float(dist), 0.4)
def test_raw_distance(self):
config['match']['distance_weights']['album'] = 3.0
config['match']['distance_weights']['medium'] = 1.0
_clear_weights()
dist = Distance()
dist.add('album', 0.5)
dist.add('medium', 0.25)
dist.add('medium', 0.5)
self.assertEqual(dist.raw_distance, 2.25)
def test_items(self):
config['match']['distance_weights']['album'] = 4.0
config['match']['distance_weights']['medium'] = 2.0
_clear_weights()
dist = Distance()
dist.add('album', 0.1875)
dist.add('medium', 0.75)
self.assertEqual(dist.items(), [('medium', 0.25), ('album', 0.125)])
# Sort by key if distance is equal.
dist = Distance()
dist.add('album', 0.375)
dist.add('medium', 0.75)
self.assertEqual(dist.items(), [('album', 0.25), ('medium', 0.25)])
def test_update(self):
dist1 = Distance()
dist1.add('album', 0.5)
dist1.add('media', 1.0)
dist2 = Distance()
dist2.add('album', 0.75)
dist2.add('album', 0.25)
dist2.add('media', 0.05)
dist1.update(dist2)
self.assertEqual(dist1._penalties, {'album': [0.5, 0.75, 0.25],
'media': [1.0, 0.05]})
class TrackDistanceTest(_common.TestCase):
def test_identical_tracks(self):
item = _make_item(u'one', 1)
info = _make_trackinfo()[0]
dist = match.track_distance(item, info, incl_artist=True)
self.assertEqual(dist, 0.0)
def test_different_title(self):
item = _make_item(u'foo', 1)
info = _make_trackinfo()[0]
dist = match.track_distance(item, info, incl_artist=True)
self.assertNotEqual(dist, 0.0)
def test_different_artist(self):
item = _make_item(u'one', 1)
item.artist = u'foo'
info = _make_trackinfo()[0]
dist = match.track_distance(item, info, incl_artist=True)
self.assertNotEqual(dist, 0.0)
def test_various_artists_tolerated(self):
item = _make_item(u'one', 1)
item.artist = u'Various Artists'
info = _make_trackinfo()[0]
dist = match.track_distance(item, info, incl_artist=True)
self.assertEqual(dist, 0.0)
class AlbumDistanceTest(_common.TestCase):
def _mapping(self, items, info):
out = {}
for i, t in zip(items, info.tracks):
out[i] = t
return out
def _dist(self, items, info):
return match.distance(items, info, self._mapping(items, info))
def test_identical_albums(self):
items = []
items.append(_make_item(u'one', 1))
items.append(_make_item(u'two', 2))
items.append(_make_item(u'three', 3))
info = AlbumInfo(
artist=u'some artist',
album=u'some album',
tracks=_make_trackinfo(),
va=False,
album_id=None,
artist_id=None,
)
self.assertEqual(self._dist(items, info), 0)
def test_incomplete_album(self):
items = []
items.append(_make_item(u'one', 1))
items.append(_make_item(u'three', 3))
info = AlbumInfo(
artist=u'some artist',
album=u'some album',
tracks=_make_trackinfo(),
va=False,
album_id=None,
artist_id=None,
)
dist = self._dist(items, info)
self.assertNotEqual(dist, 0)
# Make sure the distance is not too great
self.assertTrue(dist < 0.2)
def test_global_artists_differ(self):
items = []
items.append(_make_item(u'one', 1))
items.append(_make_item(u'two', 2))
items.append(_make_item(u'three', 3))
info = AlbumInfo(
artist=u'someone else',
album=u'some album',
tracks=_make_trackinfo(),
va=False,
album_id=None,
artist_id=None,
)
self.assertNotEqual(self._dist(items, info), 0)
def test_comp_track_artists_match(self):
items = []
items.append(_make_item(u'one', 1))
items.append(_make_item(u'two', 2))
items.append(_make_item(u'three', 3))
info = AlbumInfo(
artist=u'should be ignored',
album=u'some album',
tracks=_make_trackinfo(),
va=True,
album_id=None,
artist_id=None,
)
self.assertEqual(self._dist(items, info), 0)
def test_comp_no_track_artists(self):
# Some VA releases don't have track artists (incomplete metadata).
items = []
items.append(_make_item(u'one', 1))
items.append(_make_item(u'two', 2))
items.append(_make_item(u'three', 3))
info = AlbumInfo(
artist=u'should be ignored',
album=u'some album',
tracks=_make_trackinfo(),
va=True,
album_id=None,
artist_id=None,
)
info.tracks[0].artist = None
info.tracks[1].artist = None
info.tracks[2].artist = None
self.assertEqual(self._dist(items, info), 0)
def test_comp_track_artists_do_not_match(self):
items = []
items.append(_make_item(u'one', 1))
items.append(_make_item(u'two', 2, u'someone else'))
items.append(_make_item(u'three', 3))
info = AlbumInfo(
artist=u'some artist',
album=u'some album',
tracks=_make_trackinfo(),
va=True,
album_id=None,
artist_id=None,
)
self.assertNotEqual(self._dist(items, info), 0)
def test_tracks_out_of_order(self):
items = []
items.append(_make_item(u'one', 1))
items.append(_make_item(u'three', 2))
items.append(_make_item(u'two', 3))
info = AlbumInfo(
artist=u'some artist',
album=u'some album',
tracks=_make_trackinfo(),
va=False,
album_id=None,
artist_id=None,
)
dist = self._dist(items, info)
self.assertTrue(0 < dist < 0.2)
def test_two_medium_release(self):
items = []
items.append(_make_item(u'one', 1))
items.append(_make_item(u'two', 2))
items.append(_make_item(u'three', 3))
info = AlbumInfo(
artist=u'some artist',
album=u'some album',
tracks=_make_trackinfo(),
va=False,
album_id=None,
artist_id=None,
)
info.tracks[0].medium_index = 1
info.tracks[1].medium_index = 2
info.tracks[2].medium_index = 1
dist = self._dist(items, info)
self.assertEqual(dist, 0)
def test_per_medium_track_numbers(self):
items = []
items.append(_make_item(u'one', 1))
items.append(_make_item(u'two', 2))
items.append(_make_item(u'three', 1))
info = AlbumInfo(
artist=u'some artist',
album=u'some album',
tracks=_make_trackinfo(),
va=False,
album_id=None,
artist_id=None,
)
info.tracks[0].medium_index = 1
info.tracks[1].medium_index = 2
info.tracks[2].medium_index = 1
dist = self._dist(items, info)
self.assertEqual(dist, 0)
class AssignmentTest(unittest.TestCase):
def item(self, title, track):
return Item(
title=title, track=track,
mb_trackid='', mb_albumid='', mb_artistid='',
)
def test_reorder_when_track_numbers_incorrect(self):
items = []
items.append(self.item(u'one', 1))
items.append(self.item(u'three', 2))
items.append(self.item(u'two', 3))
trackinfo = []
trackinfo.append(TrackInfo(u'one', None))
trackinfo.append(TrackInfo(u'two', None))
trackinfo.append(TrackInfo(u'three', None))
mapping, extra_items, extra_tracks = \
match.assign_items(items, trackinfo)
self.assertEqual(extra_items, [])
self.assertEqual(extra_tracks, [])
self.assertEqual(mapping, {
items[0]: trackinfo[0],
items[1]: trackinfo[2],
items[2]: trackinfo[1],
})
def test_order_works_with_invalid_track_numbers(self):
items = []
items.append(self.item(u'one', 1))
items.append(self.item(u'three', 1))
items.append(self.item(u'two', 1))
trackinfo = []
trackinfo.append(TrackInfo(u'one', None))
trackinfo.append(TrackInfo(u'two', None))
trackinfo.append(TrackInfo(u'three', None))
mapping, extra_items, extra_tracks = \
match.assign_items(items, trackinfo)
self.assertEqual(extra_items, [])
self.assertEqual(extra_tracks, [])
self.assertEqual(mapping, {
items[0]: trackinfo[0],
items[1]: trackinfo[2],
items[2]: trackinfo[1],
})
def test_order_works_with_missing_tracks(self):
items = []
items.append(self.item(u'one', 1))
items.append(self.item(u'three', 3))
trackinfo = []
trackinfo.append(TrackInfo(u'one', None))
trackinfo.append(TrackInfo(u'two', None))
trackinfo.append(TrackInfo(u'three', None))
mapping, extra_items, extra_tracks = \
match.assign_items(items, trackinfo)
self.assertEqual(extra_items, [])
self.assertEqual(extra_tracks, [trackinfo[1]])
self.assertEqual(mapping, {
items[0]: trackinfo[0],
items[1]: trackinfo[2],
})
def test_order_works_with_extra_tracks(self):
items = []
items.append(self.item(u'one', 1))
items.append(self.item(u'two', 2))
items.append(self.item(u'three', 3))
trackinfo = []
trackinfo.append(TrackInfo(u'one', None))
trackinfo.append(TrackInfo(u'three', None))
mapping, extra_items, extra_tracks = \
match.assign_items(items, trackinfo)
self.assertEqual(extra_items, [items[1]])
self.assertEqual(extra_tracks, [])
self.assertEqual(mapping, {
items[0]: trackinfo[0],
items[2]: trackinfo[1],
})
def test_order_works_when_track_names_are_entirely_wrong(self):
# A real-world test case contributed by a user.
def item(i, length):
return Item(
artist=u'ben harper',
album=u'burn to shine',
title=u'ben harper - Burn to Shine {0}'.format(i),
track=i,
length=length,
mb_trackid='', mb_albumid='', mb_artistid='',
)
items = []
items.append(item(1, 241.37243007106997))
items.append(item(2, 342.27781704375036))
items.append(item(3, 245.95070222338137))
items.append(item(4, 472.87662515485437))
items.append(item(5, 279.1759535763187))
items.append(item(6, 270.33333768012))
items.append(item(7, 247.83435613222923))
items.append(item(8, 216.54504531525072))
items.append(item(9, 225.72775379800484))
items.append(item(10, 317.7643606963552))
items.append(item(11, 243.57001238834192))
items.append(item(12, 186.45916150485752))
def info(index, title, length):
return TrackInfo(title, None, length=length, index=index)
trackinfo = []
trackinfo.append(info(1, u'Alone', 238.893))
trackinfo.append(info(2, u'The Woman in You', 341.44))
trackinfo.append(info(3, u'Less', 245.59999999999999))
trackinfo.append(info(4, u'Two Hands of a Prayer', 470.49299999999999))
trackinfo.append(info(5, u'Please Bleed', 277.86599999999999))
trackinfo.append(info(6, u'Suzie Blue', 269.30599999999998))
trackinfo.append(info(7, u'Steal My Kisses', 245.36000000000001))
trackinfo.append(info(8, u'Burn to Shine', 214.90600000000001))
trackinfo.append(info(9, u'Show Me a Little Shame', 224.0929999999999))
trackinfo.append(info(10, u'Forgiven', 317.19999999999999))
trackinfo.append(info(11, u'Beloved One', 243.733))
trackinfo.append(info(12, u'In the Lord\'s Arms', 186.13300000000001))
mapping, extra_items, extra_tracks = \
match.assign_items(items, trackinfo)
self.assertEqual(extra_items, [])
self.assertEqual(extra_tracks, [])
for item, info in mapping.items():
self.assertEqual(items.index(item), trackinfo.index(info))
class ApplyTestUtil(object):
def _apply(self, info=None, per_disc_numbering=False):
info = info or self.info
mapping = {}
for i, t in zip(self.items, info.tracks):
mapping[i] = t
config['per_disc_numbering'] = per_disc_numbering
autotag.apply_metadata(info, mapping)
class ApplyTest(_common.TestCase, ApplyTestUtil):
def setUp(self):
super(ApplyTest, self).setUp()
self.items = []
self.items.append(Item({}))
self.items.append(Item({}))
trackinfo = []
trackinfo.append(TrackInfo(
u'oneNew',
u'dfa939ec-118c-4d0f-84a0-60f3d1e6522c',
medium=1,
medium_index=1,
medium_total=1,
index=1,
artist_credit='trackArtistCredit',
artist_sort='trackArtistSort',
))
trackinfo.append(TrackInfo(
u'twoNew',
u'40130ed1-a27c-42fd-a328-1ebefb6caef4',
medium=2,
medium_index=1,
index=2,
medium_total=1,
))
self.info = AlbumInfo(
tracks=trackinfo,
artist=u'artistNew',
album=u'albumNew',
album_id='7edb51cb-77d6-4416-a23c-3a8c2994a2c7',
artist_id='a6623d39-2d8e-4f70-8242-0a9553b91e50',
artist_credit=u'albumArtistCredit',
artist_sort=u'albumArtistSort',
albumtype=u'album',
va=False,
mediums=2,
)
def test_titles_applied(self):
self._apply()
self.assertEqual(self.items[0].title, 'oneNew')
self.assertEqual(self.items[1].title, 'twoNew')
def test_album_and_artist_applied_to_all(self):
self._apply()
self.assertEqual(self.items[0].album, 'albumNew')
self.assertEqual(self.items[1].album, 'albumNew')
self.assertEqual(self.items[0].artist, 'artistNew')
self.assertEqual(self.items[1].artist, 'artistNew')
def test_track_index_applied(self):
self._apply()
self.assertEqual(self.items[0].track, 1)
self.assertEqual(self.items[1].track, 2)
def test_track_total_applied(self):
self._apply()
self.assertEqual(self.items[0].tracktotal, 2)
self.assertEqual(self.items[1].tracktotal, 2)
def test_disc_index_applied(self):
self._apply()
self.assertEqual(self.items[0].disc, 1)
self.assertEqual(self.items[1].disc, 2)
def test_disc_total_applied(self):
self._apply()
self.assertEqual(self.items[0].disctotal, 2)
self.assertEqual(self.items[1].disctotal, 2)
def test_per_disc_numbering(self):
self._apply(per_disc_numbering=True)
self.assertEqual(self.items[0].track, 1)
self.assertEqual(self.items[1].track, 1)
def test_per_disc_numbering_track_total(self):
self._apply(per_disc_numbering=True)
self.assertEqual(self.items[0].tracktotal, 1)
self.assertEqual(self.items[1].tracktotal, 1)
def test_mb_trackid_applied(self):
self._apply()
self.assertEqual(self.items[0].mb_trackid,
'dfa939ec-118c-4d0f-84a0-60f3d1e6522c')
self.assertEqual(self.items[1].mb_trackid,
'40130ed1-a27c-42fd-a328-1ebefb6caef4')
def test_mb_albumid_and_artistid_applied(self):
self._apply()
for item in self.items:
self.assertEqual(item.mb_albumid,
'7edb51cb-77d6-4416-a23c-3a8c2994a2c7')
self.assertEqual(item.mb_artistid,
'a6623d39-2d8e-4f70-8242-0a9553b91e50')
def test_albumtype_applied(self):
self._apply()
self.assertEqual(self.items[0].albumtype, 'album')
self.assertEqual(self.items[1].albumtype, 'album')
def test_album_artist_overrides_empty_track_artist(self):
my_info = copy.deepcopy(self.info)
self._apply(info=my_info)
self.assertEqual(self.items[0].artist, 'artistNew')
self.assertEqual(self.items[1].artist, 'artistNew')
def test_album_artist_overriden_by_nonempty_track_artist(self):
my_info = copy.deepcopy(self.info)
my_info.tracks[0].artist = 'artist1!'
my_info.tracks[1].artist = 'artist2!'
self._apply(info=my_info)
self.assertEqual(self.items[0].artist, 'artist1!')
self.assertEqual(self.items[1].artist, 'artist2!')
def test_artist_credit_applied(self):
self._apply()
self.assertEqual(self.items[0].albumartist_credit, 'albumArtistCredit')
self.assertEqual(self.items[0].artist_credit, 'trackArtistCredit')
self.assertEqual(self.items[1].albumartist_credit, 'albumArtistCredit')
self.assertEqual(self.items[1].artist_credit, 'albumArtistCredit')
def test_artist_sort_applied(self):
self._apply()
self.assertEqual(self.items[0].albumartist_sort, 'albumArtistSort')
self.assertEqual(self.items[0].artist_sort, 'trackArtistSort')
self.assertEqual(self.items[1].albumartist_sort, 'albumArtistSort')
self.assertEqual(self.items[1].artist_sort, 'albumArtistSort')
def test_full_date_applied(self):
my_info = copy.deepcopy(self.info)
my_info.year = 2013
my_info.month = 12
my_info.day = 18
self._apply(info=my_info)
self.assertEqual(self.items[0].year, 2013)
self.assertEqual(self.items[0].month, 12)
self.assertEqual(self.items[0].day, 18)
def test_date_only_zeros_month_and_day(self):
self.items = []
self.items.append(Item(year=1, month=2, day=3))
self.items.append(Item(year=4, month=5, day=6))
my_info = copy.deepcopy(self.info)
my_info.year = 2013
self._apply(info=my_info)
self.assertEqual(self.items[0].year, 2013)
self.assertEqual(self.items[0].month, 0)
self.assertEqual(self.items[0].day, 0)
def test_missing_date_applies_nothing(self):
self.items = []
self.items.append(Item(year=1, month=2, day=3))
self.items.append(Item(year=4, month=5, day=6))
self._apply()
self.assertEqual(self.items[0].year, 1)
self.assertEqual(self.items[0].month, 2)
self.assertEqual(self.items[0].day, 3)
def test_data_source_applied(self):
my_info = copy.deepcopy(self.info)
my_info.data_source = 'MusicBrainz'
self._apply(info=my_info)
self.assertEqual(self.items[0].data_source, 'MusicBrainz')
class ApplyCompilationTest(_common.TestCase, ApplyTestUtil):
def setUp(self):
super(ApplyCompilationTest, self).setUp()
self.items = []
self.items.append(Item({}))
self.items.append(Item({}))
trackinfo = []
trackinfo.append(TrackInfo(
u'oneNew',
u'dfa939ec-118c-4d0f-84a0-60f3d1e6522c',
u'artistOneNew',
u'a05686fc-9db2-4c23-b99e-77f5db3e5282',
index=1,
))
trackinfo.append(TrackInfo(
u'twoNew',
u'40130ed1-a27c-42fd-a328-1ebefb6caef4',
u'artistTwoNew',
u'80b3cf5e-18fe-4c59-98c7-e5bb87210710',
index=2,
))
self.info = AlbumInfo(
tracks=trackinfo,
artist=u'variousNew',
album=u'albumNew',
album_id='3b69ea40-39b8-487f-8818-04b6eff8c21a',
artist_id='89ad4ac3-39f7-470e-963a-56509c546377',
albumtype=u'compilation',
)
def test_album_and_track_artists_separate(self):
self._apply()
self.assertEqual(self.items[0].artist, 'artistOneNew')
self.assertEqual(self.items[1].artist, 'artistTwoNew')
self.assertEqual(self.items[0].albumartist, 'variousNew')
self.assertEqual(self.items[1].albumartist, 'variousNew')
def test_mb_albumartistid_applied(self):
self._apply()
self.assertEqual(self.items[0].mb_albumartistid,
'89ad4ac3-39f7-470e-963a-56509c546377')
self.assertEqual(self.items[1].mb_albumartistid,
'89ad4ac3-39f7-470e-963a-56509c546377')
self.assertEqual(self.items[0].mb_artistid,
'a05686fc-9db2-4c23-b99e-77f5db3e5282')
self.assertEqual(self.items[1].mb_artistid,
'80b3cf5e-18fe-4c59-98c7-e5bb87210710')
def test_va_flag_cleared_does_not_set_comp(self):
self._apply()
self.assertFalse(self.items[0].comp)
self.assertFalse(self.items[1].comp)
def test_va_flag_sets_comp(self):
va_info = copy.deepcopy(self.info)
va_info.va = True
self._apply(info=va_info)
self.assertTrue(self.items[0].comp)
self.assertTrue(self.items[1].comp)
class StringDistanceTest(unittest.TestCase):
def test_equal_strings(self):
dist = string_dist(u'Some String', u'Some String')
self.assertEqual(dist, 0.0)
def test_different_strings(self):
dist = string_dist(u'Some String', u'Totally Different')
self.assertNotEqual(dist, 0.0)
def test_punctuation_ignored(self):
dist = string_dist(u'Some String', u'Some.String!')
self.assertEqual(dist, 0.0)
def test_case_ignored(self):
dist = string_dist(u'Some String', u'sOME sTring')
self.assertEqual(dist, 0.0)
def test_leading_the_has_lower_weight(self):
dist1 = string_dist(u'XXX Band Name', u'Band Name')
dist2 = string_dist(u'The Band Name', u'Band Name')
self.assertTrue(dist2 < dist1)
def test_parens_have_lower_weight(self):
dist1 = string_dist(u'One .Two.', u'One')
dist2 = string_dist(u'One (Two)', u'One')
self.assertTrue(dist2 < dist1)
def test_brackets_have_lower_weight(self):
dist1 = string_dist(u'One .Two.', u'One')
dist2 = string_dist(u'One [Two]', u'One')
self.assertTrue(dist2 < dist1)
def test_ep_label_has_zero_weight(self):
dist = string_dist(u'My Song (EP)', u'My Song')
self.assertEqual(dist, 0.0)
def test_featured_has_lower_weight(self):
dist1 = string_dist(u'My Song blah Someone', u'My Song')
dist2 = string_dist(u'My Song feat Someone', u'My Song')
self.assertTrue(dist2 < dist1)
def test_postfix_the(self):
dist = string_dist(u'The Song Title', u'Song Title, The')
self.assertEqual(dist, 0.0)
def test_postfix_a(self):
dist = string_dist(u'A Song Title', u'Song Title, A')
self.assertEqual(dist, 0.0)
def test_postfix_an(self):
dist = string_dist(u'An Album Title', u'Album Title, An')
self.assertEqual(dist, 0.0)
def test_empty_strings(self):
dist = string_dist(u'', u'')
self.assertEqual(dist, 0.0)
def test_solo_pattern(self):
# Just make sure these don't crash.
string_dist(u'The ', u'')
string_dist(u'(EP)', u'(EP)')
string_dist(u', An', u'')
def test_heuristic_does_not_harm_distance(self):
dist = string_dist(u'Untitled', u'[Untitled]')
self.assertEqual(dist, 0.0)
def test_ampersand_expansion(self):
dist = string_dist(u'And', u'&')
self.assertEqual(dist, 0.0)
def test_accented_characters(self):
dist = string_dist(u'\xe9\xe1\xf1', u'ean')
self.assertEqual(dist, 0.0)
class EnumTest(_common.TestCase):
"""
Test Enum Subclasses defined in beets.util.enumeration
"""
def test_ordered_enum(self):
OrderedEnumClass = match.OrderedEnum('OrderedEnumTest', ['a', 'b', 'c']) # noqa
self.assertLess(OrderedEnumClass.a, OrderedEnumClass.b)
self.assertLess(OrderedEnumClass.a, OrderedEnumClass.c)
self.assertLess(OrderedEnumClass.b, OrderedEnumClass.c)
self.assertGreater(OrderedEnumClass.b, OrderedEnumClass.a)
self.assertGreater(OrderedEnumClass.c, OrderedEnumClass.a)
self.assertGreater(OrderedEnumClass.c, OrderedEnumClass.b)
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/test_acousticbrainz.py 0000644 0000765 0000024 00000007357 13025125203 021205 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Nathan Dwek.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Tests for the 'acousticbrainz' plugin.
"""
from __future__ import division, absolute_import, print_function
import json
import os.path
import unittest
from test._common import RSRC
from beetsplug.acousticbrainz import AcousticPlugin, ABSCHEME
class MapDataToSchemeTest(unittest.TestCase):
def test_basic(self):
ab = AcousticPlugin()
data = {'key 1': 'value 1', 'key 2': 'value 2'}
scheme = {'key 1': 'attribute 1', 'key 2': 'attribute 2'}
mapping = set(ab._map_data_to_scheme(data, scheme))
self.assertEqual(mapping, {('attribute 1', 'value 1'),
('attribute 2', 'value 2')})
def test_recurse(self):
ab = AcousticPlugin()
data = {
'key': 'value',
'group': {
'subkey': 'subvalue',
'subgroup': {
'subsubkey': 'subsubvalue'
}
}
}
scheme = {
'key': 'attribute 1',
'group': {
'subkey': 'attribute 2',
'subgroup': {
'subsubkey': 'attribute 3'
}
}
}
mapping = set(ab._map_data_to_scheme(data, scheme))
self.assertEqual(mapping, {('attribute 1', 'value'),
('attribute 2', 'subvalue'),
('attribute 3', 'subsubvalue')})
def test_composite(self):
ab = AcousticPlugin()
data = {'key 1': 'part 1', 'key 2': 'part 2'}
scheme = {'key 1': ('attribute', 0), 'key 2': ('attribute', 1)}
mapping = set(ab._map_data_to_scheme(data, scheme))
self.assertEqual(mapping, {('attribute', 'part 1 part 2')})
def test_realistic(self):
ab = AcousticPlugin()
data_path = os.path.join(RSRC, b'acousticbrainz/data.json')
with open(data_path) as res:
data = json.load(res)
mapping = set(ab._map_data_to_scheme(data, ABSCHEME))
expected = {
('chords_key', 'A'),
('average_loudness', 0.815025985241),
('mood_acoustic', 0.415711194277),
('chords_changes_rate', 0.0445116683841),
('tonal', 0.874250173569),
('mood_sad', 0.299694597721),
('bpm', 162.532119751),
('gender', 'female'),
('initial_key', 'A minor'),
('chords_number_rate', 0.00194468453992),
('mood_relaxed', 0.123632438481),
('chords_scale', 'minor'),
('voice_instrumental', 'instrumental'),
('key_strength', 0.636936545372),
('genre_rosamerica', 'roc'),
('mood_party', 0.234383180737),
('mood_aggressive', 0.0779221653938),
('danceable', 0.143928021193),
('rhythm', 'VienneseWaltz'),
('mood_electronic', 0.339881360531),
('mood_happy', 0.0894767045975)
}
self.assertEqual(mapping, expected)
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/test_embedart.py 0000644 0000765 0000024 00000025010 13175430622 017744 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Thomas Scholtes.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
from __future__ import division, absolute_import, print_function
import os.path
import shutil
from mock import patch, MagicMock
import tempfile
import unittest
from test import _common
from test.helper import TestHelper
from beets.mediafile import MediaFile
from beets import config, logging, ui
from beets.util import syspath, displayable_path
from beets.util.artresizer import ArtResizer
from beets import art
def require_artresizer_compare(test):
def wrapper(*args, **kwargs):
if not ArtResizer.shared.can_compare:
raise unittest.SkipTest("compare not available")
else:
return test(*args, **kwargs)
wrapper.__name__ = test.__name__
return wrapper
class EmbedartCliTest(_common.TestCase, TestHelper):
small_artpath = os.path.join(_common.RSRC, b'image-2x3.jpg')
abbey_artpath = os.path.join(_common.RSRC, b'abbey.jpg')
abbey_similarpath = os.path.join(_common.RSRC, b'abbey-similar.jpg')
abbey_differentpath = os.path.join(_common.RSRC, b'abbey-different.jpg')
def setUp(self):
super(EmbedartCliTest, self).setUp()
self.io.install()
self.setup_beets() # Converter is threaded
self.load_plugins('embedart')
def _setup_data(self, artpath=None):
if not artpath:
artpath = self.small_artpath
with open(syspath(artpath), 'rb') as f:
self.image_data = f.read()
def tearDown(self):
self.unload_plugins()
self.teardown_beets()
def test_embed_art_from_file_with_yes_input(self):
self._setup_data()
album = self.add_album_fixture()
item = album.items()[0]
self.io.addinput('y')
self.run_command('embedart', '-f', self.small_artpath)
mediafile = MediaFile(syspath(item.path))
self.assertEqual(mediafile.images[0].data, self.image_data)
def test_embed_art_from_file_with_no_input(self):
self._setup_data()
album = self.add_album_fixture()
item = album.items()[0]
self.io.addinput('n')
self.run_command('embedart', '-f', self.small_artpath)
mediafile = MediaFile(syspath(item.path))
# make sure that images array is empty (nothing embedded)
self.assertEqual(len(mediafile.images), 0)
def test_embed_art_from_file(self):
self._setup_data()
album = self.add_album_fixture()
item = album.items()[0]
self.run_command('embedart', '-y', '-f', self.small_artpath)
mediafile = MediaFile(syspath(item.path))
self.assertEqual(mediafile.images[0].data, self.image_data)
def test_embed_art_from_album(self):
self._setup_data()
album = self.add_album_fixture()
item = album.items()[0]
album.artpath = self.small_artpath
album.store()
self.run_command('embedart', '-y')
mediafile = MediaFile(syspath(item.path))
self.assertEqual(mediafile.images[0].data, self.image_data)
def test_embed_art_remove_art_file(self):
self._setup_data()
album = self.add_album_fixture()
logging.getLogger('beets.embedart').setLevel(logging.DEBUG)
handle, tmp_path = tempfile.mkstemp()
os.write(handle, self.image_data)
os.close(handle)
album.artpath = tmp_path
album.store()
config['embedart']['remove_art_file'] = True
self.run_command('embedart', '-y')
if os.path.isfile(tmp_path):
os.remove(tmp_path)
self.fail(u'Artwork file {0} was not deleted'.format(tmp_path))
def test_art_file_missing(self):
self.add_album_fixture()
logging.getLogger('beets.embedart').setLevel(logging.DEBUG)
with self.assertRaises(ui.UserError):
self.run_command('embedart', '-y', '-f', '/doesnotexist')
def test_embed_non_image_file(self):
album = self.add_album_fixture()
logging.getLogger('beets.embedart').setLevel(logging.DEBUG)
handle, tmp_path = tempfile.mkstemp()
os.write(handle, b'I am not an image.')
os.close(handle)
try:
self.run_command('embedart', '-y', '-f', tmp_path)
finally:
os.remove(tmp_path)
mediafile = MediaFile(syspath(album.items()[0].path))
self.assertFalse(mediafile.images) # No image added.
@require_artresizer_compare
def test_reject_different_art(self):
self._setup_data(self.abbey_artpath)
album = self.add_album_fixture()
item = album.items()[0]
self.run_command('embedart', '-y', '-f', self.abbey_artpath)
config['embedart']['compare_threshold'] = 20
self.run_command('embedart', '-y', '-f', self.abbey_differentpath)
mediafile = MediaFile(syspath(item.path))
self.assertEqual(mediafile.images[0].data, self.image_data,
u'Image written is not {0}'.format(
displayable_path(self.abbey_artpath)))
@require_artresizer_compare
def test_accept_similar_art(self):
self._setup_data(self.abbey_similarpath)
album = self.add_album_fixture()
item = album.items()[0]
self.run_command('embedart', '-y', '-f', self.abbey_artpath)
config['embedart']['compare_threshold'] = 20
self.run_command('embedart', '-y', '-f', self.abbey_similarpath)
mediafile = MediaFile(syspath(item.path))
self.assertEqual(mediafile.images[0].data, self.image_data,
u'Image written is not {0}'.format(
displayable_path(self.abbey_similarpath)))
def test_non_ascii_album_path(self):
resource_path = os.path.join(_common.RSRC, b'image.mp3')
album = self.add_album_fixture()
trackpath = album.items()[0].path
albumpath = album.path
shutil.copy(syspath(resource_path), syspath(trackpath))
self.run_command('extractart', '-n', 'extracted')
self.assertExists(os.path.join(albumpath, b'extracted.png'))
def test_extracted_extension(self):
resource_path = os.path.join(_common.RSRC, b'image-jpeg.mp3')
album = self.add_album_fixture()
trackpath = album.items()[0].path
albumpath = album.path
shutil.copy(syspath(resource_path), syspath(trackpath))
self.run_command('extractart', '-n', 'extracted')
self.assertExists(os.path.join(albumpath, b'extracted.jpg'))
def test_clear_art_with_yes_input(self):
self._setup_data()
album = self.add_album_fixture()
item = album.items()[0]
self.io.addinput('y')
self.run_command('embedart', '-f', self.small_artpath)
self.io.addinput('y')
self.run_command('clearart')
mediafile = MediaFile(syspath(item.path))
self.assertEqual(len(mediafile.images), 0)
def test_clear_art_with_no_input(self):
self._setup_data()
album = self.add_album_fixture()
item = album.items()[0]
self.io.addinput('y')
self.run_command('embedart', '-f', self.small_artpath)
self.io.addinput('n')
self.run_command('clearart')
mediafile = MediaFile(syspath(item.path))
self.assertEqual(mediafile.images[0].data, self.image_data)
@patch('beets.art.subprocess')
@patch('beets.art.extract')
class ArtSimilarityTest(unittest.TestCase):
def setUp(self):
self.item = _common.item()
self.log = logging.getLogger('beets.embedart')
def _similarity(self, threshold):
return art.check_art_similarity(self.log, self.item, b'path',
threshold)
def _popen(self, status=0, stdout="", stderr=""):
"""Create a mock `Popen` object."""
popen = MagicMock(returncode=status)
popen.communicate.return_value = stdout, stderr
return popen
def _mock_popens(self, mock_extract, mock_subprocess, compare_status=0,
compare_stdout="", compare_stderr="", convert_status=0):
mock_extract.return_value = b'extracted_path'
mock_subprocess.Popen.side_effect = [
# The `convert` call.
self._popen(convert_status),
# The `compare` call.
self._popen(compare_status, compare_stdout, compare_stderr),
]
def test_compare_success_similar(self, mock_extract, mock_subprocess):
self._mock_popens(mock_extract, mock_subprocess, 0, "10", "err")
self.assertTrue(self._similarity(20))
def test_compare_success_different(self, mock_extract, mock_subprocess):
self._mock_popens(mock_extract, mock_subprocess, 0, "10", "err")
self.assertFalse(self._similarity(5))
def test_compare_status1_similar(self, mock_extract, mock_subprocess):
self._mock_popens(mock_extract, mock_subprocess, 1, "out", "10")
self.assertTrue(self._similarity(20))
def test_compare_status1_different(self, mock_extract, mock_subprocess):
self._mock_popens(mock_extract, mock_subprocess, 1, "out", "10")
self.assertFalse(self._similarity(5))
def test_compare_failed(self, mock_extract, mock_subprocess):
self._mock_popens(mock_extract, mock_subprocess, 2, "out", "10")
self.assertIsNone(self._similarity(20))
def test_compare_parsing_error(self, mock_extract, mock_subprocess):
self._mock_popens(mock_extract, mock_subprocess, 0, "foo", "bar")
self.assertIsNone(self._similarity(20))
def test_compare_parsing_error_and_failure(self, mock_extract,
mock_subprocess):
self._mock_popens(mock_extract, mock_subprocess, 1, "foo", "bar")
self.assertIsNone(self._similarity(20))
def test_convert_failure(self, mock_extract, mock_subprocess):
self._mock_popens(mock_extract, mock_subprocess, convert_status=1)
self.assertIsNone(self._similarity(20))
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/test_dbcore.py 0000644 0000765 0000024 00000047636 13122501062 017426 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Tests for the DBCore database abstraction.
"""
from __future__ import division, absolute_import, print_function
import os
import shutil
import sqlite3
import unittest
from six import assertRaisesRegex
from test import _common
from beets import dbcore
from tempfile import mkstemp
import six
# Fixture: concrete database and model classes. For migration tests, we
# have multiple models with different numbers of fields.
class TestSort(dbcore.query.FieldSort):
pass
class TestModel1(dbcore.Model):
_table = 'test'
_flex_table = 'testflex'
_fields = {
'id': dbcore.types.PRIMARY_ID,
'field_one': dbcore.types.INTEGER,
}
_types = {
'some_float_field': dbcore.types.FLOAT,
}
_sorts = {
'some_sort': TestSort,
}
@classmethod
def _getters(cls):
return {}
def _template_funcs(self):
return {}
class TestDatabase1(dbcore.Database):
_models = (TestModel1,)
pass
class TestModel2(TestModel1):
_fields = {
'id': dbcore.types.PRIMARY_ID,
'field_one': dbcore.types.INTEGER,
'field_two': dbcore.types.INTEGER,
}
class TestDatabase2(dbcore.Database):
_models = (TestModel2,)
pass
class TestModel3(TestModel1):
_fields = {
'id': dbcore.types.PRIMARY_ID,
'field_one': dbcore.types.INTEGER,
'field_two': dbcore.types.INTEGER,
'field_three': dbcore.types.INTEGER,
}
class TestDatabase3(dbcore.Database):
_models = (TestModel3,)
pass
class TestModel4(TestModel1):
_fields = {
'id': dbcore.types.PRIMARY_ID,
'field_one': dbcore.types.INTEGER,
'field_two': dbcore.types.INTEGER,
'field_three': dbcore.types.INTEGER,
'field_four': dbcore.types.INTEGER,
}
class TestDatabase4(dbcore.Database):
_models = (TestModel4,)
pass
class AnotherTestModel(TestModel1):
_table = 'another'
_flex_table = 'anotherflex'
_fields = {
'id': dbcore.types.PRIMARY_ID,
'foo': dbcore.types.INTEGER,
}
class TestDatabaseTwoModels(dbcore.Database):
_models = (TestModel2, AnotherTestModel)
pass
class TestModelWithGetters(dbcore.Model):
@classmethod
def _getters(cls):
return {'aComputedField': (lambda s: 'thing')}
def _template_funcs(self):
return {}
@_common.slow_test()
class MigrationTest(unittest.TestCase):
"""Tests the ability to change the database schema between
versions.
"""
@classmethod
def setUpClass(cls):
handle, cls.orig_libfile = mkstemp('orig_db')
os.close(handle)
# Set up a database with the two-field schema.
old_lib = TestDatabase2(cls.orig_libfile)
# Add an item to the old library.
old_lib._connection().execute(
'insert into test (field_one, field_two) values (4, 2)'
)
old_lib._connection().commit()
del old_lib
@classmethod
def tearDownClass(cls):
os.remove(cls.orig_libfile)
def setUp(self):
handle, self.libfile = mkstemp('db')
os.close(handle)
shutil.copyfile(self.orig_libfile, self.libfile)
def tearDown(self):
os.remove(self.libfile)
def test_open_with_same_fields_leaves_untouched(self):
new_lib = TestDatabase2(self.libfile)
c = new_lib._connection().cursor()
c.execute("select * from test")
row = c.fetchone()
self.assertEqual(len(row.keys()), len(TestModel2._fields))
def test_open_with_new_field_adds_column(self):
new_lib = TestDatabase3(self.libfile)
c = new_lib._connection().cursor()
c.execute("select * from test")
row = c.fetchone()
self.assertEqual(len(row.keys()), len(TestModel3._fields))
def test_open_with_fewer_fields_leaves_untouched(self):
new_lib = TestDatabase1(self.libfile)
c = new_lib._connection().cursor()
c.execute("select * from test")
row = c.fetchone()
self.assertEqual(len(row.keys()), len(TestModel2._fields))
def test_open_with_multiple_new_fields(self):
new_lib = TestDatabase4(self.libfile)
c = new_lib._connection().cursor()
c.execute("select * from test")
row = c.fetchone()
self.assertEqual(len(row.keys()), len(TestModel4._fields))
def test_extra_model_adds_table(self):
new_lib = TestDatabaseTwoModels(self.libfile)
try:
new_lib._connection().execute("select * from another")
except sqlite3.OperationalError:
self.fail("select failed")
class ModelTest(unittest.TestCase):
def setUp(self):
self.db = TestDatabase1(':memory:')
def tearDown(self):
self.db._connection().close()
def test_add_model(self):
model = TestModel1()
model.add(self.db)
rows = self.db._connection().execute('select * from test').fetchall()
self.assertEqual(len(rows), 1)
def test_store_fixed_field(self):
model = TestModel1()
model.add(self.db)
model.field_one = 123
model.store()
row = self.db._connection().execute('select * from test').fetchone()
self.assertEqual(row['field_one'], 123)
def test_retrieve_by_id(self):
model = TestModel1()
model.add(self.db)
other_model = self.db._get(TestModel1, model.id)
self.assertEqual(model.id, other_model.id)
def test_store_and_retrieve_flexattr(self):
model = TestModel1()
model.add(self.db)
model.foo = 'bar'
model.store()
other_model = self.db._get(TestModel1, model.id)
self.assertEqual(other_model.foo, 'bar')
def test_delete_flexattr(self):
model = TestModel1()
model['foo'] = 'bar'
self.assertTrue('foo' in model)
del model['foo']
self.assertFalse('foo' in model)
def test_delete_flexattr_via_dot(self):
model = TestModel1()
model['foo'] = 'bar'
self.assertTrue('foo' in model)
del model.foo
self.assertFalse('foo' in model)
def test_delete_flexattr_persists(self):
model = TestModel1()
model.add(self.db)
model.foo = 'bar'
model.store()
model = self.db._get(TestModel1, model.id)
del model['foo']
model.store()
model = self.db._get(TestModel1, model.id)
self.assertFalse('foo' in model)
def test_delete_non_existent_attribute(self):
model = TestModel1()
with self.assertRaises(KeyError):
del model['foo']
def test_delete_fixed_attribute(self):
model = TestModel1()
with self.assertRaises(KeyError):
del model['field_one']
def test_null_value_normalization_by_type(self):
model = TestModel1()
model.field_one = None
self.assertEqual(model.field_one, 0)
def test_null_value_stays_none_for_untyped_field(self):
model = TestModel1()
model.foo = None
self.assertEqual(model.foo, None)
def test_normalization_for_typed_flex_fields(self):
model = TestModel1()
model.some_float_field = None
self.assertEqual(model.some_float_field, 0.0)
def test_load_deleted_flex_field(self):
model1 = TestModel1()
model1['flex_field'] = True
model1.add(self.db)
model2 = self.db._get(TestModel1, model1.id)
self.assertIn('flex_field', model2)
del model1['flex_field']
model1.store()
model2.load()
self.assertNotIn('flex_field', model2)
def test_check_db_fails(self):
with assertRaisesRegex(self, ValueError, 'no database'):
dbcore.Model()._check_db()
with assertRaisesRegex(self, ValueError, 'no id'):
TestModel1(self.db)._check_db()
dbcore.Model(self.db)._check_db(need_id=False)
def test_missing_field(self):
with self.assertRaises(AttributeError):
TestModel1(self.db).nonExistingKey
def test_computed_field(self):
model = TestModelWithGetters()
self.assertEqual(model.aComputedField, 'thing')
with assertRaisesRegex(self, KeyError, u'computed field .+ deleted'):
del model.aComputedField
def test_items(self):
model = TestModel1(self.db)
model.id = 5
self.assertEqual({('id', 5), ('field_one', 0)},
set(model.items()))
def test_delete_internal_field(self):
model = dbcore.Model()
del model._db
with self.assertRaises(AttributeError):
model._db
def test_parse_nonstring(self):
with assertRaisesRegex(self, TypeError, u"must be a string"):
dbcore.Model._parse(None, 42)
class FormatTest(unittest.TestCase):
def test_format_fixed_field(self):
model = TestModel1()
model.field_one = u'caf\xe9'
value = model.formatted().get('field_one')
self.assertEqual(value, u'caf\xe9')
def test_format_flex_field(self):
model = TestModel1()
model.other_field = u'caf\xe9'
value = model.formatted().get('other_field')
self.assertEqual(value, u'caf\xe9')
def test_format_flex_field_bytes(self):
model = TestModel1()
model.other_field = u'caf\xe9'.encode('utf-8')
value = model.formatted().get('other_field')
self.assertTrue(isinstance(value, six.text_type))
self.assertEqual(value, u'caf\xe9')
def test_format_unset_field(self):
model = TestModel1()
value = model.formatted().get('other_field')
self.assertEqual(value, u'')
def test_format_typed_flex_field(self):
model = TestModel1()
model.some_float_field = 3.14159265358979
value = model.formatted().get('some_float_field')
self.assertEqual(value, u'3.1')
class FormattedMappingTest(unittest.TestCase):
def test_keys_equal_model_keys(self):
model = TestModel1()
formatted = model.formatted()
self.assertEqual(set(model.keys(True)), set(formatted.keys()))
def test_get_unset_field(self):
model = TestModel1()
formatted = model.formatted()
with self.assertRaises(KeyError):
formatted['other_field']
def test_get_method_with_default(self):
model = TestModel1()
formatted = model.formatted()
self.assertEqual(formatted.get('other_field'), u'')
def test_get_method_with_specified_default(self):
model = TestModel1()
formatted = model.formatted()
self.assertEqual(formatted.get('other_field', 'default'), 'default')
class ParseTest(unittest.TestCase):
def test_parse_fixed_field(self):
value = TestModel1._parse('field_one', u'2')
self.assertIsInstance(value, int)
self.assertEqual(value, 2)
def test_parse_flex_field(self):
value = TestModel1._parse('some_float_field', u'2')
self.assertIsInstance(value, float)
self.assertEqual(value, 2.0)
def test_parse_untyped_field(self):
value = TestModel1._parse('field_nine', u'2')
self.assertEqual(value, u'2')
class QueryParseTest(unittest.TestCase):
def pqp(self, part):
return dbcore.queryparse.parse_query_part(
part,
{'year': dbcore.query.NumericQuery},
{':': dbcore.query.RegexpQuery},
)[:-1] # remove the negate flag
def test_one_basic_term(self):
q = 'test'
r = (None, 'test', dbcore.query.SubstringQuery)
self.assertEqual(self.pqp(q), r)
def test_one_keyed_term(self):
q = 'test:val'
r = ('test', 'val', dbcore.query.SubstringQuery)
self.assertEqual(self.pqp(q), r)
def test_colon_at_end(self):
q = 'test:'
r = ('test', '', dbcore.query.SubstringQuery)
self.assertEqual(self.pqp(q), r)
def test_one_basic_regexp(self):
q = r':regexp'
r = (None, 'regexp', dbcore.query.RegexpQuery)
self.assertEqual(self.pqp(q), r)
def test_keyed_regexp(self):
q = r'test::regexp'
r = ('test', 'regexp', dbcore.query.RegexpQuery)
self.assertEqual(self.pqp(q), r)
def test_escaped_colon(self):
q = r'test\:val'
r = (None, 'test:val', dbcore.query.SubstringQuery)
self.assertEqual(self.pqp(q), r)
def test_escaped_colon_in_regexp(self):
q = r':test\:regexp'
r = (None, 'test:regexp', dbcore.query.RegexpQuery)
self.assertEqual(self.pqp(q), r)
def test_single_year(self):
q = 'year:1999'
r = ('year', '1999', dbcore.query.NumericQuery)
self.assertEqual(self.pqp(q), r)
def test_multiple_years(self):
q = 'year:1999..2010'
r = ('year', '1999..2010', dbcore.query.NumericQuery)
self.assertEqual(self.pqp(q), r)
def test_empty_query_part(self):
q = ''
r = (None, '', dbcore.query.SubstringQuery)
self.assertEqual(self.pqp(q), r)
class QueryFromStringsTest(unittest.TestCase):
def qfs(self, strings):
return dbcore.queryparse.query_from_strings(
dbcore.query.AndQuery,
TestModel1,
{':': dbcore.query.RegexpQuery},
strings,
)
def test_zero_parts(self):
q = self.qfs([])
self.assertIsInstance(q, dbcore.query.AndQuery)
self.assertEqual(len(q.subqueries), 1)
self.assertIsInstance(q.subqueries[0], dbcore.query.TrueQuery)
def test_two_parts(self):
q = self.qfs(['foo', 'bar:baz'])
self.assertIsInstance(q, dbcore.query.AndQuery)
self.assertEqual(len(q.subqueries), 2)
self.assertIsInstance(q.subqueries[0], dbcore.query.AnyFieldQuery)
self.assertIsInstance(q.subqueries[1], dbcore.query.SubstringQuery)
def test_parse_fixed_type_query(self):
q = self.qfs(['field_one:2..3'])
self.assertIsInstance(q.subqueries[0], dbcore.query.NumericQuery)
def test_parse_flex_type_query(self):
q = self.qfs(['some_float_field:2..3'])
self.assertIsInstance(q.subqueries[0], dbcore.query.NumericQuery)
def test_empty_query_part(self):
q = self.qfs([''])
self.assertIsInstance(q.subqueries[0], dbcore.query.TrueQuery)
class SortFromStringsTest(unittest.TestCase):
def sfs(self, strings):
return dbcore.queryparse.sort_from_strings(
TestModel1,
strings,
)
def test_zero_parts(self):
s = self.sfs([])
self.assertIsInstance(s, dbcore.query.NullSort)
self.assertEqual(s, dbcore.query.NullSort())
def test_one_parts(self):
s = self.sfs(['field+'])
self.assertIsInstance(s, dbcore.query.Sort)
def test_two_parts(self):
s = self.sfs(['field+', 'another_field-'])
self.assertIsInstance(s, dbcore.query.MultipleSort)
self.assertEqual(len(s.sorts), 2)
def test_fixed_field_sort(self):
s = self.sfs(['field_one+'])
self.assertIsInstance(s, dbcore.query.FixedFieldSort)
self.assertEqual(s, dbcore.query.FixedFieldSort('field_one'))
def test_flex_field_sort(self):
s = self.sfs(['flex_field+'])
self.assertIsInstance(s, dbcore.query.SlowFieldSort)
self.assertEqual(s, dbcore.query.SlowFieldSort('flex_field'))
def test_special_sort(self):
s = self.sfs(['some_sort+'])
self.assertIsInstance(s, TestSort)
class ParseSortedQueryTest(unittest.TestCase):
def psq(self, parts):
return dbcore.parse_sorted_query(
TestModel1,
parts.split(),
)
def test_and_query(self):
q, s = self.psq('foo bar')
self.assertIsInstance(q, dbcore.query.AndQuery)
self.assertIsInstance(s, dbcore.query.NullSort)
self.assertEqual(len(q.subqueries), 2)
def test_or_query(self):
q, s = self.psq('foo , bar')
self.assertIsInstance(q, dbcore.query.OrQuery)
self.assertIsInstance(s, dbcore.query.NullSort)
self.assertEqual(len(q.subqueries), 2)
def test_no_space_before_comma_or_query(self):
q, s = self.psq('foo, bar')
self.assertIsInstance(q, dbcore.query.OrQuery)
self.assertIsInstance(s, dbcore.query.NullSort)
self.assertEqual(len(q.subqueries), 2)
def test_no_spaces_or_query(self):
q, s = self.psq('foo,bar')
self.assertIsInstance(q, dbcore.query.AndQuery)
self.assertIsInstance(s, dbcore.query.NullSort)
self.assertEqual(len(q.subqueries), 1)
def test_trailing_comma_or_query(self):
q, s = self.psq('foo , bar ,')
self.assertIsInstance(q, dbcore.query.OrQuery)
self.assertIsInstance(s, dbcore.query.NullSort)
self.assertEqual(len(q.subqueries), 3)
def test_leading_comma_or_query(self):
q, s = self.psq(', foo , bar')
self.assertIsInstance(q, dbcore.query.OrQuery)
self.assertIsInstance(s, dbcore.query.NullSort)
self.assertEqual(len(q.subqueries), 3)
def test_only_direction(self):
q, s = self.psq('-')
self.assertIsInstance(q, dbcore.query.AndQuery)
self.assertIsInstance(s, dbcore.query.NullSort)
self.assertEqual(len(q.subqueries), 1)
class ResultsIteratorTest(unittest.TestCase):
def setUp(self):
self.db = TestDatabase1(':memory:')
model = TestModel1()
model['foo'] = 'baz'
model.add(self.db)
model = TestModel1()
model['foo'] = 'bar'
model.add(self.db)
def tearDown(self):
self.db._connection().close()
def test_iterate_once(self):
objs = self.db._fetch(TestModel1)
self.assertEqual(len(list(objs)), 2)
def test_iterate_twice(self):
objs = self.db._fetch(TestModel1)
list(objs)
self.assertEqual(len(list(objs)), 2)
def test_concurrent_iterators(self):
results = self.db._fetch(TestModel1)
it1 = iter(results)
it2 = iter(results)
next(it1)
list(it2)
self.assertEqual(len(list(it1)), 1)
def test_slow_query(self):
q = dbcore.query.SubstringQuery('foo', 'ba', False)
objs = self.db._fetch(TestModel1, q)
self.assertEqual(len(list(objs)), 2)
def test_slow_query_negative(self):
q = dbcore.query.SubstringQuery('foo', 'qux', False)
objs = self.db._fetch(TestModel1, q)
self.assertEqual(len(list(objs)), 0)
def test_iterate_slow_sort(self):
s = dbcore.query.SlowFieldSort('foo')
res = self.db._fetch(TestModel1, sort=s)
objs = list(res)
self.assertEqual(objs[0].foo, 'bar')
self.assertEqual(objs[1].foo, 'baz')
def test_unsorted_subscript(self):
objs = self.db._fetch(TestModel1)
self.assertEqual(objs[0].foo, 'baz')
self.assertEqual(objs[1].foo, 'bar')
def test_slow_sort_subscript(self):
s = dbcore.query.SlowFieldSort('foo')
objs = self.db._fetch(TestModel1, sort=s)
self.assertEqual(objs[0].foo, 'bar')
self.assertEqual(objs[1].foo, 'baz')
def test_length(self):
objs = self.db._fetch(TestModel1)
self.assertEqual(len(objs), 2)
def test_out_of_range(self):
objs = self.db._fetch(TestModel1)
with self.assertRaises(IndexError):
objs[100]
def test_no_results(self):
self.assertIsNone(self.db._fetch(
TestModel1, dbcore.query.FalseQuery()).get())
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/test_metasync.py 0000644 0000765 0000024 00000010721 13025125203 017775 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Tom Jaspers.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
from __future__ import division, absolute_import, print_function
import os
import platform
import time
from datetime import datetime
from beets.library import Item
from beets.util import py3_path
import unittest
from test import _common
from test.helper import TestHelper
def _parsetime(s):
return time.mktime(datetime.strptime(s, '%Y-%m-%d %H:%M:%S').timetuple())
def _is_windows():
return platform.system() == "Windows"
class MetaSyncTest(_common.TestCase, TestHelper):
itunes_library_unix = os.path.join(_common.RSRC,
b'itunes_library_unix.xml')
itunes_library_windows = os.path.join(_common.RSRC,
b'itunes_library_windows.xml')
def setUp(self):
self.setup_beets()
self.load_plugins('metasync')
self.config['metasync']['source'] = 'itunes'
if _is_windows():
self.config['metasync']['itunes']['library'] = \
py3_path(self.itunes_library_windows)
else:
self.config['metasync']['itunes']['library'] = \
py3_path(self.itunes_library_unix)
self._set_up_data()
def _set_up_data(self):
items = [_common.item() for _ in range(2)]
items[0].title = 'Tessellate'
items[0].artist = 'alt-J'
items[0].albumartist = 'alt-J'
items[0].album = 'An Awesome Wave'
items[0].itunes_rating = 60
items[1].title = 'Breezeblocks'
items[1].artist = 'alt-J'
items[1].albumartist = 'alt-J'
items[1].album = 'An Awesome Wave'
if _is_windows():
items[0].path = \
u'G:\\Music\\Alt-J\\An Awesome Wave\\03 Tessellate.mp3'
items[1].path = \
u'G:\\Music\\Alt-J\\An Awesome Wave\\04 Breezeblocks.mp3'
else:
items[0].path = u'/Music/Alt-J/An Awesome Wave/03 Tessellate.mp3'
items[1].path = u'/Music/Alt-J/An Awesome Wave/04 Breezeblocks.mp3'
for item in items:
self.lib.add(item)
def tearDown(self):
self.unload_plugins()
self.teardown_beets()
def test_load_item_types(self):
# This test also verifies that the MetaSources have loaded correctly
self.assertIn('amarok_score', Item._types)
self.assertIn('itunes_rating', Item._types)
def test_pretend_sync_from_itunes(self):
out = self.run_with_output('metasync', '-p')
self.assertIn('itunes_rating: 60 -> 80', out)
self.assertIn('itunes_rating: 100', out)
self.assertIn('itunes_playcount: 31', out)
self.assertIn('itunes_skipcount: 3', out)
self.assertIn('itunes_lastplayed: 2015-05-04 12:20:51', out)
self.assertIn('itunes_lastskipped: 2015-02-05 15:41:04', out)
self.assertEqual(self.lib.items()[0].itunes_rating, 60)
def test_sync_from_itunes(self):
self.run_command('metasync')
self.assertEqual(self.lib.items()[0].itunes_rating, 80)
self.assertEqual(self.lib.items()[0].itunes_playcount, 0)
self.assertEqual(self.lib.items()[0].itunes_skipcount, 3)
self.assertFalse(hasattr(self.lib.items()[0], 'itunes_lastplayed'))
self.assertEqual(self.lib.items()[0].itunes_lastskipped,
_parsetime('2015-02-05 15:41:04'))
self.assertEqual(self.lib.items()[1].itunes_rating, 100)
self.assertEqual(self.lib.items()[1].itunes_playcount, 31)
self.assertEqual(self.lib.items()[1].itunes_skipcount, 0)
self.assertEqual(self.lib.items()[1].itunes_lastplayed,
_parsetime('2015-05-04 12:20:51'))
self.assertFalse(hasattr(self.lib.items()[1], 'itunes_lastskipped'))
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/test_files.py 0000644 0000765 0000024 00000052306 13175434666 017307 0 ustar asampson staff 0000000 0000000 # -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Test file manipulation functionality of Item.
"""
from __future__ import division, absolute_import, print_function
import shutil
import os
import stat
from os.path import join
import unittest
from test import _common
from test._common import item, touch
import beets.library
from beets import util
from beets.util import MoveOperation
class MoveTest(_common.TestCase):
def setUp(self):
super(MoveTest, self).setUp()
# make a temporary file
self.path = join(self.temp_dir, b'temp.mp3')
shutil.copy(join(_common.RSRC, b'full.mp3'), self.path)
# add it to a temporary library
self.lib = beets.library.Library(':memory:')
self.i = beets.library.Item.from_path(self.path)
self.lib.add(self.i)
# set up the destination
self.libdir = join(self.temp_dir, b'testlibdir')
os.mkdir(self.libdir)
self.lib.directory = self.libdir
self.lib.path_formats = [('default',
join('$artist', '$album', '$title'))]
self.i.artist = 'one'
self.i.album = 'two'
self.i.title = 'three'
self.dest = join(self.libdir, b'one', b'two', b'three.mp3')
self.otherdir = join(self.temp_dir, b'testotherdir')
def test_move_arrives(self):
self.i.move()
self.assertExists(self.dest)
def test_move_to_custom_dir(self):
self.i.move(basedir=self.otherdir)
self.assertExists(join(self.otherdir, b'one', b'two', b'three.mp3'))
def test_move_departs(self):
self.i.move()
self.assertNotExists(self.path)
def test_move_in_lib_prunes_empty_dir(self):
self.i.move()
old_path = self.i.path
self.assertExists(old_path)
self.i.artist = u'newArtist'
self.i.move()
self.assertNotExists(old_path)
self.assertNotExists(os.path.dirname(old_path))
def test_copy_arrives(self):
self.i.move(operation=MoveOperation.COPY)
self.assertExists(self.dest)
def test_copy_does_not_depart(self):
self.i.move(operation=MoveOperation.COPY)
self.assertExists(self.path)
def test_move_changes_path(self):
self.i.move()
self.assertEqual(self.i.path, util.normpath(self.dest))
def test_copy_already_at_destination(self):
self.i.move()
old_path = self.i.path
self.i.move(operation=MoveOperation.COPY)
self.assertEqual(self.i.path, old_path)
def test_move_already_at_destination(self):
self.i.move()
old_path = self.i.path
self.i.move()
self.assertEqual(self.i.path, old_path)
def test_read_only_file_copied_writable(self):
# Make the source file read-only.
os.chmod(self.path, 0o444)
try:
self.i.move(operation=MoveOperation.COPY)
self.assertTrue(os.access(self.i.path, os.W_OK))
finally:
# Make everything writable so it can be cleaned up.
os.chmod(self.path, 0o777)
os.chmod(self.i.path, 0o777)
def test_move_avoids_collision_with_existing_file(self):
# Make a conflicting file at the destination.
dest = self.i.destination()
os.makedirs(os.path.dirname(dest))
touch(dest)
self.i.move()
self.assertNotEqual(self.i.path, dest)
self.assertEqual(os.path.dirname(self.i.path),
os.path.dirname(dest))
@unittest.skipUnless(_common.HAVE_SYMLINK, "need symlinks")
def test_link_arrives(self):
self.i.move(operation=MoveOperation.LINK)
self.assertExists(self.dest)
self.assertTrue(os.path.islink(self.dest))
self.assertEqual(os.readlink(self.dest), self.path)
@unittest.skipUnless(_common.HAVE_SYMLINK, "need symlinks")
def test_link_does_not_depart(self):
self.i.move(operation=MoveOperation.LINK)
self.assertExists(self.path)
@unittest.skipUnless(_common.HAVE_SYMLINK, "need symlinks")
def test_link_changes_path(self):
self.i.move(operation=MoveOperation.LINK)
self.assertEqual(self.i.path, util.normpath(self.dest))
@unittest.skipUnless(_common.HAVE_HARDLINK, "need hardlinks")
def test_hardlink_arrives(self):
self.i.move(operation=MoveOperation.HARDLINK)
self.assertExists(self.dest)
s1 = os.stat(self.path)
s2 = os.stat(self.dest)
self.assertTrue(
(s1[stat.ST_INO], s1[stat.ST_DEV]) ==
(s2[stat.ST_INO], s2[stat.ST_DEV])
)
@unittest.skipUnless(_common.HAVE_HARDLINK, "need hardlinks")
def test_hardlink_does_not_depart(self):
self.i.move(operation=MoveOperation.HARDLINK)
self.assertExists(self.path)
@unittest.skipUnless(_common.HAVE_HARDLINK, "need hardlinks")
def test_hardlink_changes_path(self):
self.i.move(operation=MoveOperation.HARDLINK)
self.assertEqual(self.i.path, util.normpath(self.dest))
class HelperTest(_common.TestCase):
def test_ancestry_works_on_file(self):
p = '/a/b/c'
a = ['/', '/a', '/a/b']
self.assertEqual(util.ancestry(p), a)
def test_ancestry_works_on_dir(self):
p = '/a/b/c/'
a = ['/', '/a', '/a/b', '/a/b/c']
self.assertEqual(util.ancestry(p), a)
def test_ancestry_works_on_relative(self):
p = 'a/b/c'
a = ['a', 'a/b']
self.assertEqual(util.ancestry(p), a)
def test_components_works_on_file(self):
p = '/a/b/c'
a = ['/', 'a', 'b', 'c']
self.assertEqual(util.components(p), a)
def test_components_works_on_dir(self):
p = '/a/b/c/'
a = ['/', 'a', 'b', 'c']
self.assertEqual(util.components(p), a)
def test_components_works_on_relative(self):
p = 'a/b/c'
a = ['a', 'b', 'c']
self.assertEqual(util.components(p), a)
class AlbumFileTest(_common.TestCase):
def setUp(self):
super(AlbumFileTest, self).setUp()
# Make library and item.
self.lib = beets.library.Library(':memory:')
self.lib.path_formats = \
[('default', join('$albumartist', '$album', '$title'))]
self.libdir = os.path.join(self.temp_dir, b'testlibdir')
self.lib.directory = self.libdir
self.i = item(self.lib)
# Make a file for the item.
self.i.path = self.i.destination()
util.mkdirall(self.i.path)
touch(self.i.path)
# Make an album.
self.ai = self.lib.add_album((self.i,))
# Alternate destination dir.
self.otherdir = os.path.join(self.temp_dir, b'testotherdir')
def test_albuminfo_move_changes_paths(self):
self.ai.album = u'newAlbumName'
self.ai.move()
self.ai.store()
self.i.load()
self.assertTrue(b'newAlbumName' in self.i.path)
def test_albuminfo_move_moves_file(self):
oldpath = self.i.path
self.ai.album = u'newAlbumName'
self.ai.move()
self.ai.store()
self.i.load()
self.assertFalse(os.path.exists(oldpath))
self.assertTrue(os.path.exists(self.i.path))
def test_albuminfo_move_copies_file(self):
oldpath = self.i.path
self.ai.album = u'newAlbumName'
self.ai.move(operation=MoveOperation.COPY)
self.ai.store()
self.i.load()
self.assertTrue(os.path.exists(oldpath))
self.assertTrue(os.path.exists(self.i.path))
def test_albuminfo_move_to_custom_dir(self):
self.ai.move(basedir=self.otherdir)
self.i.load()
self.ai.store()
self.assertTrue(b'testotherdir' in self.i.path)
class ArtFileTest(_common.TestCase):
def setUp(self):
super(ArtFileTest, self).setUp()
# Make library and item.
self.lib = beets.library.Library(':memory:')
self.libdir = os.path.join(self.temp_dir, b'testlibdir')
self.lib.directory = self.libdir
self.i = item(self.lib)
self.i.path = self.i.destination()
# Make a music file.
util.mkdirall(self.i.path)
touch(self.i.path)
# Make an album.
self.ai = self.lib.add_album((self.i,))
# Make an art file too.
self.art = self.lib.get_album(self.i).art_destination('something.jpg')
touch(self.art)
self.ai.artpath = self.art
self.ai.store()
# Alternate destination dir.
self.otherdir = os.path.join(self.temp_dir, b'testotherdir')
def test_art_deleted_when_items_deleted(self):
self.assertTrue(os.path.exists(self.art))
self.ai.remove(True)
self.assertFalse(os.path.exists(self.art))
def test_art_moves_with_album(self):
self.assertTrue(os.path.exists(self.art))
oldpath = self.i.path
self.ai.album = u'newAlbum'
self.ai.move()
self.i.load()
self.assertNotEqual(self.i.path, oldpath)
self.assertFalse(os.path.exists(self.art))
newart = self.lib.get_album(self.i).art_destination(self.art)
self.assertTrue(os.path.exists(newart))
def test_art_moves_with_album_to_custom_dir(self):
# Move the album to another directory.
self.ai.move(basedir=self.otherdir)
self.ai.store()
self.i.load()
# Art should be in new directory.
self.assertNotExists(self.art)
newart = self.lib.get_album(self.i).artpath
self.assertExists(newart)
self.assertTrue(b'testotherdir' in newart)
def test_setart_copies_image(self):
os.remove(self.art)
newart = os.path.join(self.libdir, b'newart.jpg')
touch(newart)
i2 = item()
i2.path = self.i.path
i2.artist = u'someArtist'
ai = self.lib.add_album((i2,))
i2.move(operation=MoveOperation.COPY)
self.assertEqual(ai.artpath, None)
ai.set_art(newart)
self.assertTrue(os.path.exists(ai.artpath))
def test_setart_to_existing_art_works(self):
os.remove(self.art)
# Original art.
newart = os.path.join(self.libdir, b'newart.jpg')
touch(newart)
i2 = item()
i2.path = self.i.path
i2.artist = u'someArtist'
ai = self.lib.add_album((i2,))
i2.move(operation=MoveOperation.COPY)
ai.set_art(newart)
# Set the art again.
ai.set_art(ai.artpath)
self.assertTrue(os.path.exists(ai.artpath))
def test_setart_to_existing_but_unset_art_works(self):
newart = os.path.join(self.libdir, b'newart.jpg')
touch(newart)
i2 = item()
i2.path = self.i.path
i2.artist = u'someArtist'
ai = self.lib.add_album((i2,))
i2.move(operation=MoveOperation.COPY)
# Copy the art to the destination.
artdest = ai.art_destination(newart)
shutil.copy(newart, artdest)
# Set the art again.
ai.set_art(artdest)
self.assertTrue(os.path.exists(ai.artpath))
def test_setart_to_conflicting_file_gets_new_path(self):
newart = os.path.join(self.libdir, b'newart.jpg')
touch(newart)
i2 = item()
i2.path = self.i.path
i2.artist = u'someArtist'
ai = self.lib.add_album((i2,))
i2.move(operation=MoveOperation.COPY)
# Make a file at the destination.
artdest = ai.art_destination(newart)
touch(artdest)
# Set the art.
ai.set_art(newart)
self.assertNotEqual(artdest, ai.artpath)
self.assertEqual(os.path.dirname(artdest),
os.path.dirname(ai.artpath))
def test_setart_sets_permissions(self):
os.remove(self.art)
newart = os.path.join(self.libdir, b'newart.jpg')
touch(newart)
os.chmod(newart, 0o400) # read-only
try:
i2 = item()
i2.path = self.i.path
i2.artist = u'someArtist'
ai = self.lib.add_album((i2,))
i2.move(operation=MoveOperation.COPY)
ai.set_art(newart)
mode = stat.S_IMODE(os.stat(ai.artpath).st_mode)
self.assertTrue(mode & stat.S_IRGRP)
self.assertTrue(os.access(ai.artpath, os.W_OK))
finally:
# Make everything writable so it can be cleaned up.
os.chmod(newart, 0o777)
os.chmod(ai.artpath, 0o777)
def test_move_last_file_moves_albumart(self):
oldartpath = self.lib.albums()[0].artpath
self.assertExists(oldartpath)
self.ai.album = u'different_album'
self.ai.store()
self.ai.items()[0].move()
artpath = self.lib.albums()[0].artpath
self.assertTrue(b'different_album' in artpath)
self.assertExists(artpath)
self.assertNotExists(oldartpath)
def test_move_not_last_file_does_not_move_albumart(self):
i2 = item()
i2.albumid = self.ai.id
self.lib.add(i2)
oldartpath = self.lib.albums()[0].artpath
self.assertExists(oldartpath)
self.i.album = u'different_album'
self.i.album_id = None # detach from album
self.i.move()
artpath = self.lib.albums()[0].artpath
self.assertFalse(b'different_album' in artpath)
self.assertEqual(artpath, oldartpath)
self.assertExists(oldartpath)
class RemoveTest(_common.TestCase):
def setUp(self):
super(RemoveTest, self).setUp()
# Make library and item.
self.lib = beets.library.Library(':memory:')
self.libdir = os.path.join(self.temp_dir, b'testlibdir')
self.lib.directory = self.libdir
self.i = item(self.lib)
self.i.path = self.i.destination()
# Make a music file.
util.mkdirall(self.i.path)
touch(self.i.path)
# Make an album with the item.
self.ai = self.lib.add_album((self.i,))
def test_removing_last_item_prunes_empty_dir(self):
parent = os.path.dirname(self.i.path)
self.assertExists(parent)
self.i.remove(True)
self.assertNotExists(parent)
def test_removing_last_item_preserves_nonempty_dir(self):
parent = os.path.dirname(self.i.path)
touch(os.path.join(parent, b'dummy.txt'))
self.i.remove(True)
self.assertExists(parent)
def test_removing_last_item_prunes_dir_with_blacklisted_file(self):
parent = os.path.dirname(self.i.path)
touch(os.path.join(parent, b'.DS_Store'))
self.i.remove(True)
self.assertNotExists(parent)
def test_removing_without_delete_leaves_file(self):
path = self.i.path
self.i.remove(False)
self.assertExists(path)
def test_removing_last_item_preserves_library_dir(self):
self.i.remove(True)
self.assertExists(self.libdir)
def test_removing_item_outside_of_library_deletes_nothing(self):
self.lib.directory = os.path.join(self.temp_dir, b'xxx')
parent = os.path.dirname(self.i.path)
self.i.remove(True)
self.assertExists(parent)
def test_removing_last_item_in_album_with_albumart_prunes_dir(self):
artfile = os.path.join(self.temp_dir, b'testart.jpg')
touch(artfile)
self.ai.set_art(artfile)
self.ai.store()
parent = os.path.dirname(self.i.path)
self.i.remove(True)
self.assertNotExists(parent)
# Tests that we can "delete" nonexistent files.
class SoftRemoveTest(_common.TestCase):
def setUp(self):
super(SoftRemoveTest, self).setUp()
self.path = os.path.join(self.temp_dir, b'testfile')
touch(self.path)
def test_soft_remove_deletes_file(self):
util.remove(self.path, True)
self.assertNotExists(self.path)
def test_soft_remove_silent_on_no_file(self):
try:
util.remove(self.path + b'XXX', True)
except OSError:
self.fail(u'OSError when removing path')
class SafeMoveCopyTest(_common.TestCase):
def setUp(self):
super(SafeMoveCopyTest, self).setUp()
self.path = os.path.join(self.temp_dir, b'testfile')
touch(self.path)
self.otherpath = os.path.join(self.temp_dir, b'testfile2')
touch(self.otherpath)
self.dest = self.path + b'.dest'
def test_successful_move(self):
util.move(self.path, self.dest)
self.assertExists(self.dest)
self.assertNotExists(self.path)
def test_successful_copy(self):
util.copy(self.path, self.dest)
self.assertExists(self.dest)
self.assertExists(self.path)
def test_unsuccessful_move(self):
with self.assertRaises(util.FilesystemError):
util.move(self.path, self.otherpath)
def test_unsuccessful_copy(self):
with self.assertRaises(util.FilesystemError):
util.copy(self.path, self.otherpath)
def test_self_move(self):
util.move(self.path, self.path)
self.assertExists(self.path)
def test_self_copy(self):
util.copy(self.path, self.path)
self.assertExists(self.path)
class PruneTest(_common.TestCase):
def setUp(self):
super(PruneTest, self).setUp()
self.base = os.path.join(self.temp_dir, b'testdir')
os.mkdir(self.base)
self.sub = os.path.join(self.base, b'subdir')
os.mkdir(self.sub)
def test_prune_existent_directory(self):
util.prune_dirs(self.sub, self.base)
self.assertExists(self.base)
self.assertNotExists(self.sub)
def test_prune_nonexistent_directory(self):
util.prune_dirs(os.path.join(self.sub, b'another'), self.base)
self.assertExists(self.base)
self.assertNotExists(self.sub)
class WalkTest(_common.TestCase):
def setUp(self):
super(WalkTest, self).setUp()
self.base = os.path.join(self.temp_dir, b'testdir')
os.mkdir(self.base)
touch(os.path.join(self.base, b'y'))
touch(os.path.join(self.base, b'x'))
os.mkdir(os.path.join(self.base, b'd'))
touch(os.path.join(self.base, b'd', b'z'))
def test_sorted_files(self):
res = list(util.sorted_walk(self.base))
self.assertEqual(len(res), 2)
self.assertEqual(res[0],
(self.base, [b'd'], [b'x', b'y']))
self.assertEqual(res[1],
(os.path.join(self.base, b'd'), [], [b'z']))
def test_ignore_file(self):
res = list(util.sorted_walk(self.base, (b'x',)))
self.assertEqual(len(res), 2)
self.assertEqual(res[0],
(self.base, [b'd'], [b'y']))
self.assertEqual(res[1],
(os.path.join(self.base, b'd'), [], [b'z']))
def test_ignore_directory(self):
res = list(util.sorted_walk(self.base, (b'd',)))
self.assertEqual(len(res), 1)
self.assertEqual(res[0],
(self.base, [], [b'x', b'y']))
def test_ignore_everything(self):
res = list(util.sorted_walk(self.base, (b'*',)))
self.assertEqual(len(res), 1)
self.assertEqual(res[0],
(self.base, [], []))
class UniquePathTest(_common.TestCase):
def setUp(self):
super(UniquePathTest, self).setUp()
self.base = os.path.join(self.temp_dir, b'testdir')
os.mkdir(self.base)
touch(os.path.join(self.base, b'x.mp3'))
touch(os.path.join(self.base, b'x.1.mp3'))
touch(os.path.join(self.base, b'x.2.mp3'))
touch(os.path.join(self.base, b'y.mp3'))
def test_new_file_unchanged(self):
path = util.unique_path(os.path.join(self.base, b'z.mp3'))
self.assertEqual(path, os.path.join(self.base, b'z.mp3'))
def test_conflicting_file_appends_1(self):
path = util.unique_path(os.path.join(self.base, b'y.mp3'))
self.assertEqual(path, os.path.join(self.base, b'y.1.mp3'))
def test_conflicting_file_appends_higher_number(self):
path = util.unique_path(os.path.join(self.base, b'x.mp3'))
self.assertEqual(path, os.path.join(self.base, b'x.3.mp3'))
def test_conflicting_file_with_number_increases_number(self):
path = util.unique_path(os.path.join(self.base, b'x.1.mp3'))
self.assertEqual(path, os.path.join(self.base, b'x.3.mp3'))
class MkDirAllTest(_common.TestCase):
def test_parent_exists(self):
path = os.path.join(self.temp_dir, b'foo', b'bar', b'baz', b'qux.mp3')
util.mkdirall(path)
self.assertTrue(os.path.isdir(
os.path.join(self.temp_dir, b'foo', b'bar', b'baz')
))
def test_child_does_not_exist(self):
path = os.path.join(self.temp_dir, b'foo', b'bar', b'baz', b'qux.mp3')
util.mkdirall(path)
self.assertTrue(not os.path.exists(
os.path.join(self.temp_dir, b'foo', b'bar', b'baz', b'qux.mp3')
))
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')
beets-1.4.6/test/rsrc/ 0000755 0000765 0000024 00000000000 13216774613 015532 5 ustar asampson staff 0000000 0000000 beets-1.4.6/test/rsrc/lyrics/ 0000755 0000765 0000024 00000000000 13216774613 017037 5 ustar asampson staff 0000000 0000000 beets-1.4.6/test/rsrc/lyrics/examplecom/ 0000755 0000765 0000024 00000000000 13216774613 021171 5 ustar asampson staff 0000000 0000000 beets-1.4.6/test/rsrc/lyrics/examplecom/beetssong.txt 0000644 0000765 0000024 00000070356 13025125203 023715 0 ustar asampson staff 0000000 0000000
Beets is the media library management system for obsessive-compulsive music geeks.
The purpose of beets is to get your music collection right once and for all. It catalogs your collection, automatically improving its metadata as it goes. It then provides a bouquet of tools for manipulating and accessing your music.
Here's an example of beets' brainy tag corrector doing its thing:
Because beets is designed as a library, it can do almost anything you can imagine for your music collection. Via plugins, beets becomes a panacea
Lady Madonna, children at your feet.
Wonder how you manage to make ends meet.
Who finds the money? When you pay the rent?
Did you think that money was heaven sent?
Friday night arrives without a suitcase.
Sunday morning creep in like a nun.
Monday's child has learned to tie his bootlace.
See how they run.
Lady Madonna, baby at your breast.
Wonder how you manage to feed the rest.
See how they run.
Lady Madonna, lying on the bed,
Listen to the music playing in your head.
Tuesday afternoon is never ending.
Wednesday morning papers didn't come.
Thursday night you stockings needed mending.
See how they run.
Lady Madonna, children at your feet.
Wonder how you manage to make ends meet.