├── .gitignore
├── .travis.yml
├── README.md
├── generate.py
├── get_plugin_data.py
├── plugins
├── abbreviate_artistsort
│ └── abbreviate_artistsort.py
├── abbreviate_artistsort2
│ └── abbreviate_artistsort2.py
├── acousticbrainz
│ └── acousticbrainz.py
├── acousticbrainz_tonal-rhythm
│ └── acousticbrainz_tonal-rhythm.py
├── add_album_column
│ └── __init__.py
├── addrelease
│ └── addrelease.py
├── albumartist_website
│ └── albumartist_website.py
├── albumartistextension
│ └── albumartistextension.py
├── bpm
│ ├── __init__.py
│ ├── options_bpm.ui
│ └── ui_options_bpm.py
├── classical_extras
│ ├── Readme.md
│ ├── __init__.py
│ ├── options_classical_extras.ui
│ ├── suffixtree.py
│ └── ui_options_classical_extras.py
├── classical_work_collection
│ ├── .idea
│ │ └── classical_work_collection.iml
│ ├── __init__.py
│ ├── confirm.ui
│ ├── readme.md
│ ├── select_collections.ui
│ ├── ui_confirm.py
│ ├── ui_select_collections.py
│ └── workscollection.py
├── classicdiscnumber
│ └── classicdiscnumber.py
├── cuesheet
│ └── cuesheet.py
├── decode_cyrillic
│ └── decode_cyrillic.py
├── discnumber
│ └── discnumber.py
├── fanarttv
│ ├── __init__.py
│ ├── options_fanarttv.ui
│ └── ui_options_fanarttv.py
├── featartist
│ └── featartist.py
├── featartistsintitles
│ └── featartistsintitles.py
├── keep
│ └── keep.py
├── lastfm
│ ├── __init__.py
│ ├── options_lastfm.ui
│ └── ui_options_lastfm.py
├── lastfmplus
│ ├── __init__.py
│ └── ui_options_lastfm.py
├── loadasnat
│ └── loadasnat.py
├── moodbars
│ ├── __init__.py
│ ├── options_moodbar.ui
│ └── ui_options_moodbar.py
├── musixmatch
│ ├── README
│ ├── __init__.py
│ ├── musixmatch
│ │ ├── __init__.py
│ │ ├── track.py
│ │ └── util.py
│ └── ui_options_musixmatch.py
├── no_release
│ └── no_release.py
├── non_ascii_equivalents
│ └── non_ascii_equivalents.py
├── padded
│ └── padded.py
├── papercdcase
│ └── papercdcase.py
├── playlist
│ └── playlist.py
├── release_type
│ └── release_type.py
├── remove_perfect_albums
│ └── remove_perfect_albums.py
├── reorder_sides
│ └── reorder_sides.py
├── replaygain
│ ├── __init__.py
│ ├── options_replaygain.ui
│ └── ui_options_replaygain.py
├── save_and_rewrite_header
│ └── save_and_rewrite_header.py
├── smart_title_case
│ └── smart_title_case.py
├── sort_multivalue_tags
│ └── sort_multivalue_tags.py
├── soundtrack
│ └── soundtrack.py
├── standardise_feat
│ └── standardise_feat.py
├── standardise_performers
│ └── standardise_performers.py
├── tangoinfo
│ └── tangoinfo.py
├── titlecase
│ └── titlecase.py
├── tracks2clipboard
│ └── tracks2clipboard.py
├── videotools
│ ├── __init__.py
│ ├── enzyme
│ │ ├── __init__.py
│ │ ├── asf.py
│ │ ├── core.py
│ │ ├── exceptions.py
│ │ ├── flv.py
│ │ ├── fourcc.py
│ │ ├── infos.py
│ │ ├── language.py
│ │ ├── mkv.py
│ │ ├── mp4.py
│ │ ├── mpeg.py
│ │ ├── ogm.py
│ │ ├── real.py
│ │ ├── riff.py
│ │ └── strutils.py
│ ├── formats.py
│ └── script.py
├── viewvariables
│ ├── __init__.py
│ ├── ui_variables_dialog.py
│ └── variables_dialog.ui
└── wikidata
│ └── wikidata.py
└── test.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by the script
2 | plugins.json
3 | plugins/*.zip
4 |
5 | # Byte-compiled / optimized / DLL files
6 | __pycache__/
7 | *.py[cod]
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - "2.7"
4 | script: python test.py
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MusicBrainz Picard Plugins
2 |
3 | This repository hosts plugins for [MusicBrainz Picard](https://picard.musicbrainz.org/). If you're a plugin author and would like to include your plugin here, simply open a pull request.
4 |
5 | Note that new plugins being added to the repository should be under the GNU General Public License version 2 ("GPL") or a license compatible with it. See https://www.gnu.org/licenses/license-list.html for a list of compatible licenses.
6 |
7 | ## Development Notes
8 |
9 | The script `generate.py` will generate a file called `plugins.json`, which contains metadata about all the plugins in this repository. `plugins.json` is used by [picard-website](https://github.com/musicbrainz/picard-website) and Picard itself to display information about downloadable plugins.
10 |
--------------------------------------------------------------------------------
/generate.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | from __future__ import print_function
4 | import os
5 | import re
6 | import sys
7 | import json
8 |
9 | import zipfile
10 | import zlib
11 |
12 | from hashlib import md5
13 | from subprocess import call
14 |
15 | from get_plugin_data import get_plugin_data
16 |
17 |
18 | def build_json():
19 | """
20 | Traverse the plugins directory to generate json data.
21 | """
22 |
23 | plugins = {}
24 |
25 | # All top level directories in plugin_dir are plugins
26 | for dirname in next(os.walk(plugin_dir))[1]:
27 |
28 | files = {}
29 | data = {}
30 |
31 | if dirname in [".git"]:
32 | continue
33 |
34 | dirpath = os.path.join(plugin_dir, dirname)
35 | for root, dirs, filenames in os.walk(dirpath):
36 | for filename in filenames:
37 | ext = os.path.splitext(filename)[1]
38 |
39 | if ext not in [".pyc"]:
40 | file_path = os.path.join(root, filename)
41 | with open(file_path, "rb") as md5file:
42 | md5Hash = md5(md5file.read()).hexdigest()
43 | files[file_path.split(os.path.join(dirpath, ''))[1]] = md5Hash
44 |
45 | if ext in ['.py'] and not data:
46 | data = get_plugin_data(os.path.join(plugin_dir, dirname, filename))
47 |
48 | if files and data:
49 | print("Added: " + dirname)
50 | data['files'] = files
51 | plugins[dirname] = data
52 |
53 | with open(plugin_file, "w") as out_file:
54 | json.dump({"plugins": plugins}, out_file, sort_keys=True, indent=2)
55 |
56 |
57 | def zip_files():
58 | """
59 | Zip up plugin folders
60 | """
61 |
62 | for dirname in next(os.walk(plugin_dir))[1]:
63 | archive_path = os.path.join(plugin_dir, dirname)
64 | archive = zipfile.ZipFile(archive_path + ".zip", "w")
65 |
66 | dirpath = os.path.join(plugin_dir, dirname)
67 | plugin_files = []
68 |
69 | for root, dirs, filenames in os.walk(dirpath):
70 | for filename in filenames:
71 | file_path = os.path.join(root, filename)
72 | plugin_files.append(file_path)
73 |
74 | if len(plugin_files) == 1:
75 | # There's only one file, put it directly into the zipfile
76 | archive.write(plugin_files[0],
77 | os.path.basename(plugin_files[0]),
78 | compress_type=zipfile.ZIP_DEFLATED)
79 | else:
80 | for filename in plugin_files:
81 | # Preserve the folder structure relative to plugin_dir
82 | # in the zip file
83 | name_in_zip = os.path.join(os.path.relpath(filename,
84 | plugin_dir))
85 | archive.write(filename,
86 | name_in_zip,
87 | compress_type=zipfile.ZIP_DEFLATED)
88 |
89 | print("Created: " + dirname + ".zip")
90 |
91 |
92 | # The file that contains json data
93 | plugin_file = "plugins.json"
94 |
95 | # The directory which contains plugin files
96 | plugin_dir = "plugins"
97 |
98 | if __name__ == '__main__':
99 | if 1 in sys.argv:
100 | if sys.argv[1] == "pull":
101 | call(["git", "pull", "-q"])
102 | elif sys.argv[1] == "json":
103 | build_json()
104 | elif sys.argv[1] == "zip":
105 | zip_files()
106 | else:
107 | # call(["git", "pull", "-q"])
108 | build_json()
109 | zip_files()
110 |
--------------------------------------------------------------------------------
/get_plugin_data.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from __future__ import print_function
4 | import ast
5 |
6 | KNOWN_DATA = [
7 | 'PLUGIN_NAME',
8 | 'PLUGIN_AUTHOR',
9 | 'PLUGIN_VERSION',
10 | 'PLUGIN_API_VERSIONS',
11 | 'PLUGIN_LICENSE',
12 | 'PLUGIN_LICENSE_URL',
13 | 'PLUGIN_DESCRIPTION',
14 | ]
15 |
16 |
17 | def get_plugin_data(filepath):
18 | """Parse a python file and return a dict with plugin metadata"""
19 | data = {}
20 | with open(filepath, 'rU') as plugin_file:
21 | source = plugin_file.read()
22 | try:
23 | root = ast.parse(source, filepath)
24 | except:
25 | print("Cannot parse " + filepath)
26 | raise
27 | for node in ast.iter_child_nodes(root):
28 | if isinstance(node, ast.Assign) and len(node.targets) == 1:
29 | target = node.targets[0]
30 | if (isinstance(target, ast.Name)
31 | and isinstance(target.ctx, ast.Store)
32 | and target.id in KNOWN_DATA):
33 | name = target.id.replace('PLUGIN_', '', 1).lower()
34 | if name not in data:
35 | try:
36 | data[name] = ast.literal_eval(node.value)
37 | except ValueError:
38 | print('Cannot evaluate value in '
39 | + filepath + ':' +
40 | ast.dump(node))
41 | return data
42 |
--------------------------------------------------------------------------------
/plugins/abbreviate_artistsort2/abbreviate_artistsort2.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python2
2 | # -*- coding: utf-8 -*-
3 |
4 | # This is the Abbreviate Artist plugin for MusicBrainz Picard.
5 | # Copyright (C) 2013-2017 Sophist
6 | #
7 | # This program is free software; you can redistribute it and/or
8 | # modify it under the terms of the GNU General Public License
9 | # as published by the Free Software Foundation; either version 2
10 | # of the License, or (at your option) any later version.
11 | #
12 | # This program is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 |
17 | PLUGIN_NAME = u"Abbreviate Artist-Sort v2"
18 | PLUGIN_AUTHOR = u"Sophist"
19 | PLUGIN_DESCRIPTION = u'''
20 | Abbreviate Artist-Sort and Album-Artist-Sort Tags.
21 | e.g. "Vivaldi, Antonio" becomes "Vivaldi A"
22 | This is particularly useful for classical albums that can have a long list of artists.
23 | This version of the plugin differs from version 1 as it modifies the relevant metadata
24 | in place (rather than copying it into new variables.
25 | '''
26 | PLUGIN_VERSION = "1.0"
27 | PLUGIN_API_VERSIONS = ["1.4"]
28 | PLUGIN_LICENSE = "GPL-2.0"
29 | PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"
30 |
31 | import re, unicodedata
32 |
33 | artist_tags = [
34 | ('artistsort', '~artists_sort', '~unabbrev_artistsort', '~unabbrev_artists_sort'),
35 | ('albumartistsort', '~albumartists_sort', '~unabbrev_albumartistsort', '~unabbrev_albumartists_sort'),
36 | ]
37 |
38 | def abbreviate_name(artist):
39 | if u"," not in artist:
40 | return artist
41 | surname, forenames = artist.split(u",", 1)
42 | return surname + u" " + "".join([x[0] for x in re.split(ur"\W+", forenames.strip())])
43 |
44 | def string_cleanup(string, locale="utf-8"):
45 | if not string:
46 | return u""
47 | if not isinstance(string, unicode):
48 | string = string.decode(locale)
49 | # Replace with normalised unicode string
50 | return unicodedata.normalize("NFKC", string)
51 |
52 | def abbreviate_artist(artist, locale="utf-8"):
53 | """Title-case a string using a less destructive method than str.title."""
54 | return abbreviate_name(string_cleanup(artist, locale))
55 |
56 | assert "Bach JS" == abbreviate_artist("Bach, Johann Sebastian")
57 | assert "The Beatles" == abbreviate_artist("The Beatles")
58 |
59 | def abbreviate_artistsort(text, artists, abbrev_artists):
60 | """
61 | Use the array of artists and the joined string
62 | to identify artists to make title case
63 | and the join strings to leave as-is.
64 | """
65 | find = u"^(" + ur")(\s*\S+\s*)(".join((map(re.escape, map(string_cleanup,artists)))) + u")(.*$)"
66 | replace = "".join([ur"%s\%d" % (a, x*2 + 2) for x, a in enumerate(abbrev_artists)])
67 | result = re.sub(find, replace, string_cleanup(text), re.UNICODE)
68 | return result
69 |
70 | assert "Bach JS; London Symphony Orchestra" == abbreviate_artistsort(
71 | "Bach, Johann Sebastian; London Symphony Orchestra",
72 | ["Bach, Johann Sebastian", "London Symphony Orchestra"],
73 | ["Bach JS", "London Symphony Orchestra"],
74 | )
75 |
76 | # Put this here so that above unit tests can run standalone before getting an import error
77 | from picard import log
78 | from picard.metadata import (
79 | register_track_metadata_processor,
80 | register_album_metadata_processor,
81 | )
82 |
83 | def abbrev_artistsort_metadata(tagger, metadata, release, track=None):
84 | for artist_string, artists_list, original_artist, original_artists in artist_tags:
85 | if artist_string in metadata and artists_list in metadata:
86 | artists = metadata.getall(artists_list)
87 | artist = metadata.getall(artist_string)
88 | abbrev_artists = map(abbreviate_artist, artists)
89 | abbrev_artist = [abbreviate_artistsort(x, artists, abbrev_artists) for x in artist]
90 | if artists != abbrev_artists and artist != abbrev_artist:
91 | log.debug("AbbrevArtistSort2: Abbreviated %s from %r to %r", artists_list, artists, abbrev_artists)
92 | metadata[original_artists] = artists
93 | metadata[artists_list] = abbrev_artists
94 | log.debug("AbbrevArtistSort2: Abbreviated %s from %r to %r", artist_string, artist, abbrev_artist)
95 | metadata[original_artist] = artist
96 | metadata[artist_string] = abbrev_artist
97 | elif artists != abbrev_artists or artist != abbrev_artist:
98 | if artists != abbrev_artists:
99 | log.warning("AbbrevArtistSort2: %s abbreviated, %s wasn't", artists_list, artist_string)
100 | else:
101 | log.warning("AbbrevArtistSort2: %s abbreviated, %s wasn't", artist_string, artists_list)
102 |
103 | register_track_metadata_processor(abbrev_artistsort_metadata)
104 | register_album_metadata_processor(abbrev_artistsort_metadata)
105 |
--------------------------------------------------------------------------------
/plugins/acousticbrainz/acousticbrainz.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Acousticbrainz plugin for Picard
3 | # Copyright (C) 2015 Andrew Cook
4 | #
5 | # This program is free software; you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License version 2 as
7 | # published by the Free Software Foundation.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License along
15 | # with this program; if not, write to the Free Software Foundation, Inc.,
16 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17 | #
18 |
19 | PLUGIN_NAME = u'AcousticBrainz Mood-Genre'
20 | PLUGIN_AUTHOR = u'Andrew Cook'
21 | PLUGIN_DESCRIPTION = u'''Uses AcousticBrainz for mood and genre.
22 |
23 | WARNING: Experimental plugin. All guarantees voided by use.'''
24 | PLUGIN_LICENSE = "GPL-2.0"
25 | PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.txt"
26 | PLUGIN_VERSION = "0.0"
27 | PLUGIN_API_VERSIONS = ["0.15"]
28 |
29 | from json import loads
30 | from functools import partial
31 | from picard import log
32 | from picard.metadata import register_track_metadata_processor
33 | from picard.webservice import REQUEST_DELAY
34 |
35 | ACOUSTICBRAINZ_HOST = "acousticbrainz.org"
36 | ACOUSTICBRAINZ_PORT = 80
37 |
38 | REQUEST_DELAY[(ACOUSTICBRAINZ_HOST, ACOUSTICBRAINZ_PORT)] = 50
39 |
40 | def result(album, metadata, data, reply, error):
41 | moods = []
42 | genres = []
43 | try:
44 | data = loads(data)["highlevel"]
45 | for k, v in data.items():
46 | if k.startswith("genre_") and not v["value"].startswith("not_"):
47 | genres.append(v["value"])
48 | if k.startswith("mood_") and not v["value"].startswith("not_"):
49 | moods.append(v["value"])
50 |
51 | metadata["genre"] = genres
52 | metadata["mood"] = moods
53 | log.debug(u"%s: Track %s (%s) Parsed response (genres: %s, moods: %s)", PLUGIN_NAME, metadata["musicbrainz_recordingid"], metadata["title"], str(genres), str(moods))
54 | except Exception as e:
55 | log.error(u"%s: Track %s (%s) Error parsing response: %s", PLUGIN_NAME, metadata["musicbrainz_recordingid"], metadata["title"], str(e))
56 | finally:
57 | album._requests -= 1
58 | album._finalize_loading(None)
59 |
60 | def process_track(album, metadata, release, track):
61 | album.tagger.xmlws.download(
62 | ACOUSTICBRAINZ_HOST,
63 | ACOUSTICBRAINZ_PORT,
64 | u"/%s/high-level" % (metadata["musicbrainz_recordingid"]),
65 | partial(result, album, metadata),
66 | priority=True
67 | )
68 | album._requests += 1
69 |
70 | register_track_metadata_processor(process_track)
71 |
--------------------------------------------------------------------------------
/plugins/acousticbrainz_tonal-rhythm/acousticbrainz_tonal-rhythm.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Acousticbrainz Tonal/Rhythm plugin for Picard
3 | # Copyright (C) 2015 Sophist
4 | #
5 | # This program is free software; you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License version 2 as
7 | # published by the Free Software Foundation.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License along
15 | # with this program; if not, write to the Free Software Foundation, Inc.,
16 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17 | #
18 |
19 | PLUGIN_NAME = u'AcousticBrainz Tonal-Rhythm'
20 | PLUGIN_AUTHOR = u'Sophist'
21 | PLUGIN_DESCRIPTION = u'''Add's the following tags:
22 |
23 | Key (in ID3v2.3 format)
24 | Beats Per Minute (BPM)
25 |
26 | from the AcousticBrainz database.
27 | Note: This plugin requires Picard 1.4.'''
28 | PLUGIN_LICENSE = "GPL-2.0"
29 | PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.txt"
30 | PLUGIN_VERSION = '0.1'
31 | PLUGIN_API_VERSIONS = ["1.4.0"] # Requires support for TKEY which is in 1.4
32 |
33 | import json
34 | from picard import config, log
35 | from picard.util import LockableObject
36 | from picard.metadata import register_track_metadata_processor
37 | from functools import partial
38 | from picard.webservice import REQUEST_DELAY
39 |
40 | ACOUSTICBRAINZ_HOST = "acousticbrainz.org"
41 | ACOUSTICBRAINZ_PORT = 80
42 | REQUEST_DELAY[(ACOUSTICBRAINZ_HOST, ACOUSTICBRAINZ_PORT)] = 50
43 |
44 | class AcousticBrainz_Key:
45 |
46 | def get_data(self, album, track_metadata, trackXmlNode, releaseXmlNode):
47 | recordingId = track_metadata['musicbrainz_recordingid']
48 | if recordingId:
49 | log.debug("%s: Add AcusticBrainz request for %s (%s)", PLUGIN_NAME, track_metadata['title'], recordingId)
50 | self.album_add_request(album)
51 | path = "/%s/low-level" % recordingId
52 | return album.tagger.xmlws.get(
53 | ACOUSTICBRAINZ_HOST,
54 | ACOUSTICBRAINZ_PORT,
55 | path,
56 | partial(self.process_data, album, track_metadata),
57 | xml=False, priority=True, important=False)
58 | return
59 |
60 | def process_data(self, album, track_metadata, response, reply, error):
61 | if error:
62 | log.error("%s: Network error retrieving acousticBrainz data for recordingId %s",
63 | PLUGIN_NAME, track_metadata['musicbrainz_recordingid'])
64 | self.album_remove_request(album)
65 | return
66 | data = json.loads(response)
67 | if "tonal" in data:
68 | if "key_key" in data["tonal"]:
69 | key = data["tonal"]["key_key"]
70 | if "key_scale" in data["tonal"]:
71 | scale = data["tonal"]["key_scale"]
72 | if scale == "minor":
73 | key += "m"
74 | track_metadata["key"] = key
75 | log.debug("%s: Track '%s' is in key %s", PLUGIN_NAME, track_metadata["title"], key)
76 | if "rhythm" in data:
77 | if "bpm" in data["rhythm"]:
78 | bpm = int(data["rhythm"]["bpm"] + 0.5)
79 | track_metadata["bpm"] = bpm
80 | log.debug("%s: Track '%s' has %s bpm", PLUGIN_NAME, track_metadata["title"], bpm)
81 | self.album_remove_request(album)
82 |
83 | def album_add_request(self, album):
84 | album._requests += 1
85 |
86 | def album_remove_request(self, album):
87 | album._requests -= 1
88 | if album._requests == 0:
89 | album._finalize_loading(None)
90 |
91 | register_track_metadata_processor(AcousticBrainz_Key().get_data)
92 |
--------------------------------------------------------------------------------
/plugins/add_album_column/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 |
3 | #
4 | # Licensing
5 | #
6 | # Add Album Column, Add the Album column to the main window panel
7 | # Copyright (C) 2017 Evandro Coan
8 | #
9 | # This program is free software; you can redistribute it and/or modify it
10 | # under the terms of the GNU General Public License as published by the
11 | # Free Software Foundation; either version 3 of the License, or ( at
12 | # your option ) any later version.
13 | #
14 | # This program is distributed in the hope that it will be useful, but
15 | # WITHOUT ANY WARRANTY; without even the implied warranty of
16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17 | # General Public License for more details.
18 | #
19 | # You should have received a copy of the GNU General Public License
20 | # along with this program. If not, see .
21 | #
22 |
23 | PLUGIN_NAME = u"Add Album Column"
24 | PLUGIN_AUTHOR = u"Evandro Coan"
25 | PLUGIN_DESCRIPTION = "Add the Album column to the main window panel."
26 |
27 | PLUGIN_VERSION = "1.0"
28 | PLUGIN_API_VERSIONS = ["1.4.0"]
29 | PLUGIN_LICENSE = "GPLv3"
30 | PLUGIN_LICENSE_URL = "http://www.gnu.org/licenses/"
31 |
32 | from picard.ui.itemviews import MainPanel
33 | MainPanel.columns.append((N_('Album'), 'album'))
34 |
--------------------------------------------------------------------------------
/plugins/albumartist_website/albumartist_website.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | PLUGIN_NAME = u'Album Artist Website'
4 | PLUGIN_AUTHOR = u'Sophist'
5 | PLUGIN_DESCRIPTION = u'''Add's the album artist(s) Official Homepage(s)
6 | (if they are defined in the MusicBrainz database).'''
7 | PLUGIN_VERSION = '0.6'
8 | PLUGIN_API_VERSIONS = ["1.4.0"]
9 | PLUGIN_LICENSE = "GPL-2.0"
10 | PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"
11 |
12 | from picard import config, log
13 | from picard.util import LockableObject
14 | from picard.metadata import register_track_metadata_processor
15 | from functools import partial
16 |
17 |
18 | class AlbumArtistWebsite:
19 |
20 | class ArtistWebsiteQueue(LockableObject):
21 |
22 | def __init__(self):
23 | LockableObject.__init__(self)
24 | self.queue = {}
25 |
26 | def __contains__(self, name):
27 | return name in self.queue
28 |
29 | def __iter__(self):
30 | return self.queue.__iter__()
31 |
32 | def __getitem__(self, name):
33 | self.lock_for_read()
34 | value = self.queue[name] if name in self.queue else None
35 | self.unlock()
36 | return value
37 |
38 | def __setitem__(self, name, value):
39 | self.lock_for_write()
40 | self.queue[name] = value
41 | self.unlock()
42 |
43 | def append(self, name, value):
44 | self.lock_for_write()
45 | if name in self.queue:
46 | self.queue[name].append(value)
47 | value = False
48 | else:
49 | self.queue[name] = [value]
50 | value = True
51 | self.unlock()
52 | return value
53 |
54 | def remove(self, name):
55 | self.lock_for_write()
56 | value = None
57 | if name in self.queue:
58 | value = self.queue[name]
59 | del self.queue[name]
60 | self.unlock()
61 | return value
62 |
63 | def __init__(self):
64 | self.website_cache = {}
65 | self.website_queue = self.ArtistWebsiteQueue()
66 |
67 | def add_artist_website(self, album, track_metadata, trackXmlNode, releaseXmlNode):
68 | albumArtistIds = dict.get(track_metadata,'musicbrainz_albumartistid', [])
69 | for artistId in albumArtistIds:
70 | if artistId in self.website_cache:
71 | if self.website_cache[artistId]:
72 | track_metadata['website'] = self.website_cache[artistId]
73 | else:
74 | # Jump through hoops to get track object!!
75 | self.website_add_track(album, album._new_tracks[-1], artistId)
76 |
77 | def website_add_track(self, album, track, artistId):
78 | self.album_add_request(album)
79 | if self.website_queue.append(artistId, (track, album)):
80 | host = config.setting["server_host"]
81 | port = config.setting["server_port"]
82 | path = "/ws/2/%s/%s" % ('artist', artistId)
83 | queryargs = {"inc": "url-rels"}
84 | return album.tagger.xmlws.get(host, port, path,
85 | partial(self.website_process, artistId),
86 | xml=True, priority=True, important=False,
87 | queryargs=queryargs)
88 |
89 | def website_process(self, artistId, response, reply, error):
90 | if error:
91 | log.error("%s: %r: Network error retrieving artist record", PLUGIN_NAME, artistId)
92 | tuples = self.website_queue.remove(artistId)
93 | for track, album in tuples:
94 | self.album_remove_request(album)
95 | return
96 | urls = self.artist_process_metadata(artistId, response)
97 | self.website_cache[artistId] = urls
98 | tuples = self.website_queue.remove(artistId)
99 | log.debug("%s: %r: Artist Official Homepages = %r", PLUGIN_NAME,
100 | artistId, urls)
101 | for track, album in tuples:
102 | if urls:
103 | tm = track.metadata
104 | tm['website'] = urls
105 | for file in track.iterfiles(True):
106 | fm = file.metadata
107 | fm['website'] = urls
108 | self.album_remove_request(album)
109 |
110 | def album_add_request(self, album):
111 | album._requests += 1
112 |
113 | def album_remove_request(self, album):
114 | album._requests -= 1
115 | album._finalize_loading(None)
116 |
117 | def artist_process_metadata(self, artistId, response):
118 | if 'metadata' in response.children:
119 | if 'artist' in response.metadata[0].children:
120 | if 'relation_list' in response.metadata[0].artist[0].children:
121 | if 'relation' in response.metadata[0].artist[0].relation_list[0].children:
122 | return self.artist_process_relations(response.metadata[0].artist[0].relation_list[0].relation)
123 | else:
124 | log.error("%s: %r: MusicBrainz artist xml result not in correct format - %s",
125 | PLUGIN_NAME, artistId, response)
126 | return None
127 |
128 | def artist_process_relations(self, relations):
129 | urls = []
130 | for relation in relations:
131 | if relation.type == 'official homepage' \
132 | and 'target' in relation.children:
133 | urls.append(relation.target[0].text)
134 | return sorted(urls)
135 |
136 |
137 | register_track_metadata_processor(AlbumArtistWebsite().add_artist_website)
138 |
--------------------------------------------------------------------------------
/plugins/albumartistextension/albumartistextension.py:
--------------------------------------------------------------------------------
1 | PLUGIN_NAME = 'AlbumArtist Extension'
2 | PLUGIN_AUTHOR = 'Bob Swift (rdswift)'
3 | PLUGIN_DESCRIPTION = '''
4 | This plugin provides standardized, credited and sorted artist information
5 | for the album artist. This is useful when your tagging or renaming scripts
6 | require both the standardized artist name and the credited artist name, or
7 | more detailed information about the album artists.
8 |
9 | The information is provided in the following variables:
10 |
11 | _aaeStdAlbumArtists = The standardized version of the album artists.
12 | _aaeCredAlbumArtists = The credited version of the album artists.
13 | _aaeSortAlbumArtists = The sorted version of the album artists.
14 | _aaeStdPrimaryAlbumArtist = The standardized version of the first
15 | (primary) album artist.
16 | _aaeCredPrimaryAlbumArtist = The credited version of the first (primary)
17 | album artist.
18 | _aaeSortPrimaryAlbumArtist = The sorted version of the first (primary)
19 | album artist.
20 | _aaeAlbumArtistCount = The number of artists comprising the album artist.
21 |
22 | PLEASE NOTE: Tagger scripts are required to make use of these hidden
23 | variables.
24 | '''
25 |
26 | PLUGIN_VERSION = "0.5"
27 | PLUGIN_API_VERSIONS = ["1.4"]
28 | PLUGIN_LICENSE = "GPL-2.0 or later"
29 | PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"
30 |
31 | from picard import config, log
32 | from picard.metadata import register_album_metadata_processor
33 | from picard.plugin import PluginPriority
34 |
35 | class AlbumArtistStdName:
36 |
37 | @staticmethod
38 | def add_artist_std_name(album, album_metadata, releaseXmlNode):
39 | albumid = releaseXmlNode.id
40 | # Test for valid XML node for the release
41 | if 'artist_credit' in releaseXmlNode.children:
42 | # Initialize variables to default values
43 | credArtist = ""
44 | stdArtist = ""
45 | sortArtist = ""
46 | aCount = 0
47 | # Get the current lists of _albumartists and _albumartists_sort
48 | metaAlbumArtists = dict.get(album_metadata,"~albumartists")
49 | metaAlbumArtists_Sort = dict.get(album_metadata,"~albumartists_sort")
50 | # The 'name_credit' child should always be there.
51 | # This check is to avoid a runtime error if it doesn't exist for some reason.
52 | if 'name_credit' in releaseXmlNode.artist_credit[0].children:
53 | for ncredit in releaseXmlNode.artist_credit[0].name_credit:
54 | # Initialize temporary variables for each loop.
55 | tempStdName = ""
56 | tempCredName = ""
57 | tempSortName = ""
58 | tempPhrase = ""
59 | # Check if there is a 'joinphrase' specified.
60 | if 'joinphrase' in ncredit.attribs:
61 | tempPhrase = ncredit.joinphrase
62 | # Set the credit name from the AlbumArtist list if the
63 | # 'Use standardized artist name' option is not checked
64 | # in Picard, otherwise use the XML information.
65 | if config.setting["standardize_artists"]:
66 | # Check if there is a 'name' specified. This will be the
67 | # credited name.
68 | if 'name' in ncredit.children:
69 | tempCredName = ncredit.name[0].text
70 | else:
71 | tempCredName = metaAlbumArtists[aCount]
72 | # The 'artist' child should always be there.
73 | # This check is to avoid a runtime error if it doesn't
74 | # exist for some reason.
75 | if 'artist' in ncredit.children:
76 | # The 'name' child should always be there.
77 | # This check is to avoid a runtime error if it
78 | # doesn't exist for some reason.
79 | if 'name' in ncredit.artist[0].children:
80 | # Set the standardized name from the AlbumArtist
81 | # list if the 'Use standardized artist name'
82 | # option is checked in Picard, otherwise use the
83 | # XML information.
84 | tempStdName = metaAlbumArtists[aCount] if config.setting["standardize_artists"] else ncredit.artist[0].name[0].text
85 | stdArtist += tempStdName + tempPhrase
86 | tCredName = tempCredName if len(tempCredName) > 0 else tempStdName
87 | credArtist += tCredName + tempPhrase
88 | if aCount < 1:
89 | album_metadata["~aaeStdPrimaryAlbumArtist"] = tempStdName
90 | album_metadata["~aaeCredPrimaryAlbumArtist"] = tCredName
91 | else:
92 | log.error("%s: %r: Missing artist 'name' in XML contents: %s",
93 | PLUGIN_NAME, albumid, releaseXmlNode)
94 | # Get the artist sort name from the
95 | # _albumartists_sort list
96 | tempSortName = metaAlbumArtists_Sort[aCount]
97 | sortArtist += tempSortName + tempPhrase
98 | if aCount < 1:
99 | album_metadata["~aaeSortPrimaryAlbumArtist"] = tempSortName
100 | else:
101 | log.error("%s: %r: Missing 'artist' in XML contents: %s",
102 | PLUGIN_NAME, albumid, releaseXmlNode)
103 | aCount += 1
104 | else:
105 | log.error("%s: %r: Missing 'name_credit' in XML contents: %s",
106 | PLUGIN_NAME, albumid, releaseXmlNode)
107 | if len(stdArtist) > 0:
108 | album_metadata["~aaeStdAlbumArtists"] = stdArtist
109 | if len(credArtist) > 0:
110 | album_metadata["~aaeCredAlbumArtists"] = credArtist
111 | if len(sortArtist) > 0:
112 | album_metadata["~aaeSortAlbumArtists"] = sortArtist
113 | if aCount > 0:
114 | album_metadata["~aaeAlbumArtistCount"] = aCount
115 | else:
116 | log.error("%s: %r: Error reading XML contents: %s",
117 | PLUGIN_NAME, albumid, releaseXmlNode)
118 | return None
119 |
120 | # Register the plugin to run at a LOW priority so that other plugins that
121 | # modify the contents of the _albumartists and _albumartists_sort lists can
122 | # complete their processing and this plugin is working with the latest
123 | # updated data.
124 | register_album_metadata_processor(AlbumArtistStdName().add_artist_std_name, priority=PluginPriority.LOW)
125 |
--------------------------------------------------------------------------------
/plugins/bpm/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Changelog:
4 | # [2015-09-15] Initial version
5 | # Dependancies:
6 | # aubio, numpy
7 | #
8 |
9 | PLUGIN_NAME = u"BPM Analyzer"
10 | PLUGIN_AUTHOR = u"Len Joubert"
11 | PLUGIN_DESCRIPTION = """Calculate BPM for selected files and albums."""
12 | PLUGIN_LICENSE = "GPL-2.0"
13 | PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"
14 | PLUGIN_VERSION = "0.1"
15 | PLUGIN_API_VERSIONS = ["0.10", "0.15", "0.16"]
16 | #PLUGIN_INCOMPATIBLE_PLATFORMS = [
17 | # 'win32', 'cygwyn', 'darwin', 'os2', 'os2emx', 'riscos', 'atheos']
18 |
19 | from collections import defaultdict
20 | from subprocess import check_call
21 | from picard.album import Album, NatAlbum
22 | from picard.track import Track
23 | from picard.file import File
24 | from picard.util import encode_filename, decode_filename, partial, thread
25 | from picard.ui.options import register_options_page, OptionsPage
26 | from picard.config import TextOption, IntOption
27 | from picard.ui.itemviews import (BaseAction, register_file_action,
28 | register_album_action)
29 | from picard.plugins.bpm.ui_options_bpm import Ui_BPMOptionsPage
30 | from aubio import source, tempo
31 | from numpy import median, diff
32 |
33 |
34 | bpm_slider_settings = {
35 | 1: (44100, 1024, 512),
36 | 2: (8000, 512, 128),
37 | 3: (4000, 128, 64),
38 | }
39 |
40 |
41 | def get_file_bpm(self, path):
42 | """ Calculate the beats per minute (bpm) of a given file.
43 | path: path to the file
44 | buf_size length of FFT
45 | hop_size number of frames between two consecutive runs
46 | samplerate sampling rate of the signal to analyze
47 | """
48 |
49 | samplerate, buf_size, hop_size = bpm_slider_settings[
50 | BPMOptionsPage.config.setting["bpm_slider_parameter"]]
51 | mediasource = source(path.encode("utf-8"), samplerate, hop_size)
52 | samplerate = mediasource.samplerate
53 | beattracking = tempo("specdiff", buf_size, hop_size, samplerate)
54 | # List of beats, in samples
55 | beats = []
56 | # Total number of frames read
57 | total_frames = 0
58 |
59 | while True:
60 | samples, read = mediasource()
61 | is_beat = beattracking(samples)
62 | if is_beat:
63 | this_beat = beattracking.get_last_s()
64 | beats.append(this_beat)
65 | total_frames += read
66 | if read < hop_size:
67 | break
68 |
69 | # Convert to periods and to bpm
70 | bpms = 60. / diff(beats)
71 | return median(bpms)
72 |
73 |
74 | class FileBPM(BaseAction):
75 | NAME = N_("Calculate BPM...")
76 |
77 | def _add_file_to_queue(self, file):
78 | thread.run_task(
79 | partial(self._calculate_bpm, file),
80 | partial(self._calculate_bpm_callback, file))
81 |
82 | def callback(self, objs):
83 | for obj in objs:
84 | if isinstance(obj, Track):
85 | for file_ in obj.linked_files:
86 | self._add_file_to_queue(file_)
87 | elif isinstance(obj, File):
88 | self._add_file_to_queue(obj)
89 |
90 | def _calculate_bpm(self, file):
91 | self.tagger.window.set_statusbar_message(
92 | N_('Calculating BPM for "%(filename)s"...'),
93 | {'filename': file.filename}
94 | )
95 | calculated_bpm = get_file_bpm(self.tagger, file.filename)
96 | # self.tagger.log.debug('%s' % (calculated_bpm))
97 | file.metadata["bpm"] = str(round(calculated_bpm, 1))
98 |
99 | def _calculate_bpm_callback(self, file, result=None, error=None):
100 | if not error:
101 | self.tagger.window.set_statusbar_message(
102 | N_('BPM for "%(filename)s" successfully calculated.'),
103 | {'filename': file.filename}
104 | )
105 | else:
106 | self.tagger.window.set_statusbar_message(
107 | N_('Could not calculate BPM for "%(filename)s".'),
108 | {'filename': file.filename}
109 | )
110 |
111 |
112 | class BPMOptionsPage(OptionsPage):
113 |
114 | NAME = "bpm"
115 | TITLE = "BPM"
116 | PARENT = "plugins"
117 | ACTIVE = True
118 |
119 | options = [
120 | IntOption("setting", "bpm_slider_parameter", 1)
121 | ]
122 |
123 | def __init__(self, parent=None):
124 | super(BPMOptionsPage, self).__init__(parent)
125 | self.ui = Ui_BPMOptionsPage()
126 | self.ui.setupUi(self)
127 | self.ui.slider_parameter.valueChanged.connect(self.update_parameters)
128 |
129 | def load(self):
130 | cfg = self.config.setting
131 | self.ui.slider_parameter.setValue(cfg["bpm_slider_parameter"])
132 |
133 | def save(self):
134 | cfg = self.config.setting
135 | cfg["bpm_slider_parameter"] = self.ui.slider_parameter.value()
136 |
137 | def update_parameters(self):
138 | val = self.ui.slider_parameter.value()
139 | samplerate, buf_size, hop_size = [unicode(v) for v in
140 | bpm_slider_settings[val]]
141 | self.ui.samplerate_value.setText(samplerate)
142 | self.ui.win_s_value.setText(buf_size)
143 | self.ui.hop_s_value.setText(hop_size)
144 |
145 |
146 | register_file_action(FileBPM())
147 | register_options_page(BPMOptionsPage)
148 |
--------------------------------------------------------------------------------
/plugins/bpm/options_bpm.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | BPMOptionsPage
4 |
5 |
6 |
7 | 0
8 | 0
9 | 495
10 | 273
11 |
12 |
13 |
14 | -
15 |
16 |
17 | BPM Analyze Parameters:
18 |
19 |
20 |
21 |
22 | 8
23 | 80
24 | 43
25 | 19
26 |
27 |
28 |
29 |
30 |
31 |
32 | Default
33 |
34 |
35 |
36 |
37 |
38 | 20
39 | 51
40 | 391
41 | 23
42 |
43 |
44 |
45 | 1
46 |
47 |
48 | 3
49 |
50 |
51 | Qt::Horizontal
52 |
53 |
54 | QSlider::TicksBelow
55 |
56 |
57 | 1
58 |
59 |
60 |
61 |
62 |
63 | 370
64 | 80
65 | 61
66 | 19
67 |
68 |
69 |
70 | Qt::RightToLeft
71 |
72 |
73 | Super Fast
74 |
75 |
76 |
77 |
78 |
79 | 10
80 | 120
81 | 471
82 | 20
83 |
84 |
85 |
86 | Qt::Horizontal
87 |
88 |
89 |
90 |
91 |
92 | 6
93 | 140
94 | 471
95 | 91
96 |
97 |
98 |
99 | -
100 |
101 |
102 |
103 |
104 |
105 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
106 |
107 |
108 |
109 | -
110 |
111 |
112 | Number of frames between two consecutive runs:
113 |
114 |
115 |
116 | -
117 |
118 |
119 |
120 |
121 |
122 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
123 |
124 |
125 |
126 | -
127 |
128 |
129 |
130 |
131 |
132 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
133 |
134 |
135 |
136 | -
137 |
138 |
139 | Length of FFT:
140 |
141 |
142 |
143 | -
144 |
145 |
146 | Samplerate:
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
--------------------------------------------------------------------------------
/plugins/bpm/ui_options_bpm.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Form implementation generated from reading ui file 'options_bpm2.ui'
4 | #
5 | # Created: Wed Sep 23 09:41:53 2015
6 | # by: PyQt4 UI code generator 4.11.3
7 | #
8 | # WARNING! All changes made in this file will be lost!
9 |
10 | from PyQt4 import QtCore, QtGui
11 |
12 | try:
13 | _fromUtf8 = QtCore.QString.fromUtf8
14 | except AttributeError:
15 | def _fromUtf8(s):
16 | return s
17 |
18 | try:
19 | _encoding = QtGui.QApplication.UnicodeUTF8
20 | def _translate(context, text, disambig):
21 | return QtGui.QApplication.translate(context, text, disambig, _encoding)
22 | except AttributeError:
23 | def _translate(context, text, disambig):
24 | return QtGui.QApplication.translate(context, text, disambig)
25 |
26 | class Ui_BPMOptionsPage(object):
27 | def setupUi(self, BPMOptionsPage):
28 | BPMOptionsPage.setObjectName(_fromUtf8("BPMOptionsPage"))
29 | BPMOptionsPage.resize(495, 273)
30 | self.gridLayout_2 = QtGui.QGridLayout(BPMOptionsPage)
31 | self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2"))
32 | self.bpm_options = QtGui.QGroupBox(BPMOptionsPage)
33 | self.bpm_options.setObjectName(_fromUtf8("bpm_options"))
34 | self.slider_default = QtGui.QLabel(self.bpm_options)
35 | self.slider_default.setGeometry(QtCore.QRect(8, 80, 43, 19))
36 | self.slider_default.setToolTip(_fromUtf8(""))
37 | self.slider_default.setObjectName(_fromUtf8("slider_default"))
38 | self.slider_parameter = QtGui.QSlider(self.bpm_options)
39 | self.slider_parameter.setGeometry(QtCore.QRect(20, 51, 391, 23))
40 | self.slider_parameter.setMinimum(1)
41 | self.slider_parameter.setMaximum(3)
42 | self.slider_parameter.setOrientation(QtCore.Qt.Horizontal)
43 | self.slider_parameter.setTickPosition(QtGui.QSlider.TicksBelow)
44 | self.slider_parameter.setTickInterval(1)
45 | self.slider_parameter.setObjectName(_fromUtf8("slider_parameter"))
46 | self.slider_super_fast = QtGui.QLabel(self.bpm_options)
47 | self.slider_super_fast.setGeometry(QtCore.QRect(370, 80, 61, 19))
48 | self.slider_super_fast.setLayoutDirection(QtCore.Qt.RightToLeft)
49 | self.slider_super_fast.setObjectName(_fromUtf8("slider_super_fast"))
50 | self.line = QtGui.QFrame(self.bpm_options)
51 | self.line.setGeometry(QtCore.QRect(10, 120, 471, 20))
52 | self.line.setFrameShape(QtGui.QFrame.HLine)
53 | self.line.setFrameShadow(QtGui.QFrame.Sunken)
54 | self.line.setObjectName(_fromUtf8("line"))
55 | self.horizontalLayoutWidget = QtGui.QWidget(self.bpm_options)
56 | self.horizontalLayoutWidget.setGeometry(QtCore.QRect(6, 140, 471, 91))
57 | self.horizontalLayoutWidget.setObjectName(_fromUtf8("horizontalLayoutWidget"))
58 | self.gridLayout = QtGui.QGridLayout(self.horizontalLayoutWidget)
59 | self.gridLayout.setMargin(0)
60 | self.gridLayout.setObjectName(_fromUtf8("gridLayout"))
61 | self.samplerate_value = QtGui.QLabel(self.horizontalLayoutWidget)
62 | self.samplerate_value.setText(_fromUtf8(""))
63 | self.samplerate_value.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
64 | self.samplerate_value.setObjectName(_fromUtf8("samplerate_value"))
65 | self.gridLayout.addWidget(self.samplerate_value, 2, 1, 1, 1)
66 | self.hop_s_label = QtGui.QLabel(self.horizontalLayoutWidget)
67 | self.hop_s_label.setObjectName(_fromUtf8("hop_s_label"))
68 | self.gridLayout.addWidget(self.hop_s_label, 1, 0, 1, 1)
69 | self.win_s_value = QtGui.QLabel(self.horizontalLayoutWidget)
70 | self.win_s_value.setText(_fromUtf8(""))
71 | self.win_s_value.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
72 | self.win_s_value.setObjectName(_fromUtf8("win_s_value"))
73 | self.gridLayout.addWidget(self.win_s_value, 0, 1, 1, 1)
74 | self.hop_s_value = QtGui.QLabel(self.horizontalLayoutWidget)
75 | self.hop_s_value.setText(_fromUtf8(""))
76 | self.hop_s_value.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
77 | self.hop_s_value.setObjectName(_fromUtf8("hop_s_value"))
78 | self.gridLayout.addWidget(self.hop_s_value, 1, 1, 1, 1)
79 | self.win_s_label = QtGui.QLabel(self.horizontalLayoutWidget)
80 | self.win_s_label.setObjectName(_fromUtf8("win_s_label"))
81 | self.gridLayout.addWidget(self.win_s_label, 0, 0, 1, 1)
82 | self.samplerate_label = QtGui.QLabel(self.horizontalLayoutWidget)
83 | self.samplerate_label.setObjectName(_fromUtf8("samplerate_label"))
84 | self.gridLayout.addWidget(self.samplerate_label, 2, 0, 1, 1)
85 | self.gridLayout.setColumnStretch(0, 1)
86 | self.gridLayout.setColumnStretch(1, 1)
87 | self.gridLayout_2.addWidget(self.bpm_options, 0, 0, 1, 1)
88 |
89 | self.retranslateUi(BPMOptionsPage)
90 | QtCore.QMetaObject.connectSlotsByName(BPMOptionsPage)
91 |
92 | def retranslateUi(self, BPMOptionsPage):
93 | self.bpm_options.setTitle(_translate("BPMOptionsPage", "BPM Analyze Parameters:", None))
94 | self.slider_default.setText(_translate("BPMOptionsPage", "Default", None))
95 | self.slider_super_fast.setText(_translate("BPMOptionsPage", "Super Fast", None))
96 | self.hop_s_label.setText(_translate("BPMOptionsPage", "Number of frames between two consecutive runs:", None))
97 | self.win_s_label.setText(_translate("BPMOptionsPage", "Length of FFT:", None))
98 | self.samplerate_label.setText(_translate("BPMOptionsPage", "Samplerate:", None))
99 |
100 |
--------------------------------------------------------------------------------
/plugins/classical_work_collection/.idea/classical_work_collection.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/plugins/classical_work_collection/confirm.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | ConfirmDialog
4 |
5 |
6 |
7 | 0
8 | 0
9 | 440
10 | 130
11 |
12 |
13 |
14 | Dialog
15 |
16 |
17 |
18 |
19 | 20
20 | 20
21 | 391
22 | 71
23 |
24 |
25 |
26 | Please confirm:-
27 |
28 |
29 |
30 |
31 | 10
32 | 30
33 | 371
34 | 16
35 |
36 |
37 |
38 | Adding xxxxxx works to the collection "Collection"
39 |
40 |
41 |
42 |
43 |
44 | 10
45 | 50
46 | 381
47 | 16
48 |
49 |
50 |
51 | All xxxxx selected works are already in the collection - no more will be added.
52 |
53 |
54 |
55 |
56 |
57 |
58 | 10
59 | 90
60 | 341
61 | 32
62 |
63 |
64 |
65 | Qt::Horizontal
66 |
67 |
68 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok
69 |
70 |
71 |
72 |
73 |
74 |
75 | buttonBox
76 | accepted()
77 | ConfirmDialog
78 | accept()
79 |
80 |
81 | 238
82 | 94
83 |
84 |
85 | 157
86 | 274
87 |
88 |
89 |
90 |
91 | buttonBox
92 | rejected()
93 | ConfirmDialog
94 | reject()
95 |
96 |
97 | 306
98 | 100
99 |
100 |
101 | 286
102 | 274
103 |
104 |
105 |
106 |
107 |
108 |
--------------------------------------------------------------------------------
/plugins/classical_work_collection/readme.md:
--------------------------------------------------------------------------------
1 | This is the documentation for version 0.1 "Classical Work Collections". There may be beta versions later than this - check [my github site](https://github.com/MetaTunes/picard-plugins/releases) for newer releases.
2 |
3 | This plugin adds a context menu 'add works to collections', which operates from track or album selections
4 | regardless of whether a file is present. It presents a dialog box showing available work collections. Select the
5 | collection(s)and a confirmation dialog appears. Confirming will add works from all the selected tracks to the
6 | selected collections.
7 | If the plugin 'Classical Extras' has been used then all parent works will also be added.
8 |
9 | The first dialog box gives options:
10 | * Maximum number of works to be added at a time: The default is 200. More than this may result in "URI too large" error (even though the MB documentation says 400 should work). If a "URI too large" error occurs, reduce the limit."
11 | * Provide analysis of existing collection and new works before updating: Selecting this (the default) will provide information about how many of the selected works are already in the selected collection(s) and only new works will be submitted. Deselecting it will result in all selected works being submitted, but will almost certainly be faster as existing works can only be looked up at the rate of 100 per sec.
12 |
13 | Assuming the default on the second option above, the second dialog box (one per collection) will provide the analysis described.
14 |
--------------------------------------------------------------------------------
/plugins/classical_work_collection/select_collections.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | CollectionsDialog
4 |
5 |
6 |
7 | 0
8 | 0
9 | 408
10 | 334
11 |
12 |
13 |
14 | Dialog
15 |
16 |
17 |
18 |
19 | 40
20 | 280
21 | 341
22 | 32
23 |
24 |
25 |
26 | Qt::Horizontal
27 |
28 |
29 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok
30 |
31 |
32 |
33 |
34 |
35 | 0
36 | 10
37 | 401
38 | 161
39 |
40 |
41 |
42 | Select collections:-
43 |
44 |
45 |
46 |
47 | 10
48 | 20
49 | 371
50 | 101
51 |
52 |
53 |
54 | QAbstractItemView::MultiSelection
55 |
56 |
57 |
58 |
59 |
60 | 10
61 | 130
62 | 431
63 | 16
64 |
65 |
66 |
67 | Highlight the collections into which to add the works from the selected tracks
68 |
69 |
70 |
71 |
72 |
73 |
74 | 310
75 | 170
76 | 71
77 | 22
78 |
79 |
80 |
81 | 1
82 |
83 |
84 | 400
85 |
86 |
87 |
88 |
89 |
90 | 20
91 | 170
92 | 281
93 | 16
94 |
95 |
96 |
97 | Maximum number of works to be added at a time
98 |
99 |
100 |
101 |
102 |
103 | 20
104 | 200
105 | 261
106 | 16
107 |
108 |
109 |
110 | (multiple submissions will be generated automatically
111 |
112 |
113 |
114 |
115 |
116 | 20
117 | 210
118 | 291
119 | 16
120 |
121 |
122 |
123 | if the total number of works to be added exceeds this max)
124 |
125 |
126 |
127 |
128 |
129 | 20
130 | 230
131 | 361
132 | 31
133 |
134 |
135 |
136 | Qt::RightToLeft
137 |
138 |
139 | Provide analysis of existing collection and new works before updating?
140 |
141 |
142 |
143 |
144 |
145 | 20
146 | 250
147 | 371
148 | 16
149 |
150 |
151 |
152 | (Faster if unchecked, but less informative)
153 |
154 |
155 |
156 |
157 |
158 | 20
159 | 180
160 | 261
161 | 16
162 |
163 |
164 |
165 | More than 200 may result in "URI too large" error
166 |
167 |
168 |
169 |
170 |
171 |
172 | buttonBox
173 | accepted()
174 | CollectionsDialog
175 | accept()
176 |
177 |
178 | 258
179 | 264
180 |
181 |
182 | 157
183 | 274
184 |
185 |
186 |
187 |
188 | buttonBox
189 | rejected()
190 | CollectionsDialog
191 | reject()
192 |
193 |
194 | 316
195 | 260
196 |
197 |
198 | 286
199 | 274
200 |
201 |
202 |
203 |
204 |
205 |
--------------------------------------------------------------------------------
/plugins/classical_work_collection/ui_confirm.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Form implementation generated from reading ui file 'C:\Users\Mark\Documents\Mark's documents\Music\Picard\Classical Works Collection development\classical_work_collection\confirm.ui'
4 | #
5 | # Created: Wed May 16 11:07:22 2018
6 | # by: PyQt4 UI code generator 4.10
7 | #
8 | # WARNING! All changes made in this file will be lost!
9 |
10 | from PyQt4 import QtCore, QtGui
11 |
12 | try:
13 | _fromUtf8 = QtCore.QString.fromUtf8
14 | except AttributeError:
15 | def _fromUtf8(s):
16 | return s
17 |
18 | try:
19 | _encoding = QtGui.QApplication.UnicodeUTF8
20 | def _translate(context, text, disambig):
21 | return QtGui.QApplication.translate(context, text, disambig, _encoding)
22 | except AttributeError:
23 | def _translate(context, text, disambig):
24 | return QtGui.QApplication.translate(context, text, disambig)
25 |
26 | class Ui_ConfirmDialog(object):
27 | def setupUi(self, ConfirmDialog):
28 | ConfirmDialog.setObjectName(_fromUtf8("ConfirmDialog"))
29 | ConfirmDialog.resize(440, 130)
30 | self.groupBox = QtGui.QGroupBox(ConfirmDialog)
31 | self.groupBox.setGeometry(QtCore.QRect(20, 20, 391, 71))
32 | self.groupBox.setObjectName(_fromUtf8("groupBox"))
33 | self.label = QtGui.QLabel(self.groupBox)
34 | self.label.setGeometry(QtCore.QRect(10, 30, 371, 16))
35 | self.label.setObjectName(_fromUtf8("label"))
36 | self.label_2 = QtGui.QLabel(self.groupBox)
37 | self.label_2.setGeometry(QtCore.QRect(10, 50, 381, 16))
38 | self.label_2.setObjectName(_fromUtf8("label_2"))
39 | self.buttonBox = QtGui.QDialogButtonBox(ConfirmDialog)
40 | self.buttonBox.setGeometry(QtCore.QRect(10, 90, 341, 32))
41 | self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
42 | self.buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Cancel|QtGui.QDialogButtonBox.Ok)
43 | self.buttonBox.setObjectName(_fromUtf8("buttonBox"))
44 |
45 | self.retranslateUi(ConfirmDialog)
46 | QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL(_fromUtf8("accepted()")), ConfirmDialog.accept)
47 | QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL(_fromUtf8("rejected()")), ConfirmDialog.reject)
48 | QtCore.QMetaObject.connectSlotsByName(ConfirmDialog)
49 |
50 | def retranslateUi(self, ConfirmDialog):
51 | ConfirmDialog.setWindowTitle(_translate("ConfirmDialog", "Dialog", None))
52 | self.groupBox.setTitle(_translate("ConfirmDialog", "Please confirm:-", None))
53 | self.label.setText(_translate("ConfirmDialog", "Adding xxxxxx works to the collection \"Collection\"", None))
54 | self.label_2.setText(_translate("ConfirmDialog", "All xxxxx selected works are already in the collection - no more will be added.", None))
55 |
56 |
--------------------------------------------------------------------------------
/plugins/classical_work_collection/ui_select_collections.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Form implementation generated from reading ui file 'C:\Users\Mark\Documents\Mark's documents\Music\Picard\Classical Works Collection development\classical_work_collection\select_collections.ui'
4 | #
5 | # Created: Thu May 17 14:20:56 2018
6 | # by: PyQt4 UI code generator 4.10
7 | #
8 | # WARNING! All changes made in this file will be lost!
9 |
10 | from PyQt4 import QtCore, QtGui
11 |
12 | try:
13 | _fromUtf8 = QtCore.QString.fromUtf8
14 | except AttributeError:
15 | def _fromUtf8(s):
16 | return s
17 |
18 | try:
19 | _encoding = QtGui.QApplication.UnicodeUTF8
20 | def _translate(context, text, disambig):
21 | return QtGui.QApplication.translate(context, text, disambig, _encoding)
22 | except AttributeError:
23 | def _translate(context, text, disambig):
24 | return QtGui.QApplication.translate(context, text, disambig)
25 |
26 | class Ui_CollectionsDialog(object):
27 | def setupUi(self, CollectionsDialog):
28 | CollectionsDialog.setObjectName(_fromUtf8("CollectionsDialog"))
29 | CollectionsDialog.resize(408, 334)
30 | self.buttonBox = QtGui.QDialogButtonBox(CollectionsDialog)
31 | self.buttonBox.setGeometry(QtCore.QRect(40, 280, 341, 32))
32 | self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
33 | self.buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Cancel|QtGui.QDialogButtonBox.Ok)
34 | self.buttonBox.setObjectName(_fromUtf8("buttonBox"))
35 | self.groupBox = QtGui.QGroupBox(CollectionsDialog)
36 | self.groupBox.setGeometry(QtCore.QRect(0, 10, 401, 161))
37 | self.groupBox.setObjectName(_fromUtf8("groupBox"))
38 | self.collection_list = QtGui.QListWidget(self.groupBox)
39 | self.collection_list.setGeometry(QtCore.QRect(10, 20, 371, 101))
40 | self.collection_list.setSelectionMode(QtGui.QAbstractItemView.MultiSelection)
41 | self.collection_list.setObjectName(_fromUtf8("collection_list"))
42 | self.label = QtGui.QLabel(self.groupBox)
43 | self.label.setGeometry(QtCore.QRect(10, 130, 431, 16))
44 | self.label.setObjectName(_fromUtf8("label"))
45 | self.max_works = QtGui.QSpinBox(CollectionsDialog)
46 | self.max_works.setGeometry(QtCore.QRect(310, 170, 71, 22))
47 | self.max_works.setMinimum(1)
48 | self.max_works.setMaximum(400)
49 | self.max_works.setObjectName(_fromUtf8("max_works"))
50 | self.label_2 = QtGui.QLabel(CollectionsDialog)
51 | self.label_2.setGeometry(QtCore.QRect(20, 170, 281, 16))
52 | self.label_2.setObjectName(_fromUtf8("label_2"))
53 | self.label_3 = QtGui.QLabel(CollectionsDialog)
54 | self.label_3.setGeometry(QtCore.QRect(20, 200, 261, 16))
55 | self.label_3.setObjectName(_fromUtf8("label_3"))
56 | self.label_4 = QtGui.QLabel(CollectionsDialog)
57 | self.label_4.setGeometry(QtCore.QRect(20, 210, 291, 16))
58 | self.label_4.setObjectName(_fromUtf8("label_4"))
59 | self.provide_analysis = QtGui.QCheckBox(CollectionsDialog)
60 | self.provide_analysis.setGeometry(QtCore.QRect(20, 230, 361, 31))
61 | self.provide_analysis.setLayoutDirection(QtCore.Qt.RightToLeft)
62 | self.provide_analysis.setObjectName(_fromUtf8("provide_analysis"))
63 | self.label_5 = QtGui.QLabel(CollectionsDialog)
64 | self.label_5.setGeometry(QtCore.QRect(20, 250, 371, 16))
65 | self.label_5.setObjectName(_fromUtf8("label_5"))
66 | self.label_7 = QtGui.QLabel(CollectionsDialog)
67 | self.label_7.setGeometry(QtCore.QRect(20, 180, 261, 16))
68 | self.label_7.setObjectName(_fromUtf8("label_7"))
69 |
70 | self.retranslateUi(CollectionsDialog)
71 | QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL(_fromUtf8("accepted()")), CollectionsDialog.accept)
72 | QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL(_fromUtf8("rejected()")), CollectionsDialog.reject)
73 | QtCore.QMetaObject.connectSlotsByName(CollectionsDialog)
74 |
75 | def retranslateUi(self, CollectionsDialog):
76 | CollectionsDialog.setWindowTitle(_translate("CollectionsDialog", "Dialog", None))
77 | self.groupBox.setTitle(_translate("CollectionsDialog", "Select collections:-", None))
78 | self.label.setText(_translate("CollectionsDialog", "Highlight the collections into which to add the works from the selected tracks", None))
79 | self.label_2.setText(_translate("CollectionsDialog", "Maximum number of works to be added at a time", None))
80 | self.label_3.setText(_translate("CollectionsDialog", "(multiple submissions will be generated automatically", None))
81 | self.label_4.setText(_translate("CollectionsDialog", "if the total number of works to be added exceeds this max)", None))
82 | self.provide_analysis.setText(_translate("CollectionsDialog", "Provide analysis of existing collection and new works before updating? ", None))
83 | self.label_5.setText(_translate("CollectionsDialog", "(Faster if unchecked, but less informative)", None))
84 | self.label_7.setText(_translate("CollectionsDialog", "More than 200 may result in \"URI too large\" error", None))
85 |
86 |
--------------------------------------------------------------------------------
/plugins/classicdiscnumber/classicdiscnumber.py:
--------------------------------------------------------------------------------
1 | PLUGIN_NAME = 'Classic Disc Numbers'
2 | PLUGIN_AUTHOR = 'Lukas Lalinsky'
3 | PLUGIN_DESCRIPTION = '''Moves disc numbers and subtitles from the separate tags to album titles.'''
4 | PLUGIN_VERSION = "0.1"
5 | PLUGIN_API_VERSIONS = ["0.15"]
6 |
7 | from picard.metadata import register_track_metadata_processor
8 | import re
9 |
10 |
11 | def add_discnumbers(tagger, metadata, release, track):
12 | if int(metadata["totaldiscs"] or "0") > 1:
13 | if "discsubtitle" in metadata:
14 | metadata["album"] = "%s (disc %s: %s)" % (
15 | metadata["album"], metadata["discnumber"],
16 | metadata["discsubtitle"])
17 | else:
18 | metadata["album"] = "%s (disc %s)" % (
19 | metadata["album"], metadata["discnumber"])
20 |
21 | register_track_metadata_processor(add_discnumbers)
22 |
--------------------------------------------------------------------------------
/plugins/cuesheet/cuesheet.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | PLUGIN_NAME = u"Generate Cuesheet"
4 | PLUGIN_AUTHOR = u"Lukáš Lalinský"
5 | PLUGIN_DESCRIPTION = "Generate cuesheet (.cue file) from an album."
6 | PLUGIN_VERSION = "0.2"
7 | PLUGIN_API_VERSIONS = ["0.10", "0.15"]
8 |
9 |
10 | import os.path
11 | import re
12 | from PyQt4 import QtCore, QtGui
13 | from picard.util import find_existing_path, encode_filename
14 | from picard.ui.itemviews import BaseAction, register_album_action
15 |
16 |
17 | _whitespace_re = re.compile('\s', re.UNICODE)
18 | _split_re = re.compile('\s*("[^"]*"|[^ ]+)\s*', re.UNICODE)
19 |
20 |
21 | def msfToMs(msf):
22 | msf = msf.split(":")
23 | return ((int(msf[0]) * 60 + int(msf[1])) * 75 + int(msf[2])) * 1000 / 75
24 |
25 |
26 | class CuesheetTrack(list):
27 |
28 | def __init__(self, cuesheet, index):
29 | list.__init__(self)
30 | self.cuesheet = cuesheet
31 | self.index = index
32 |
33 | def set(self, *args):
34 | self.append(args)
35 |
36 | def find(self, prefix):
37 | return [i for i in self if tuple(i[:len(prefix)]) == tuple(prefix)]
38 |
39 | def getTrackNumber(self):
40 | return self.index
41 |
42 | def getLength(self):
43 | try:
44 | nextTrack = self.cuesheet.tracks[self.index + 1]
45 | index0 = self.find((u"INDEX", u"01"))
46 | index1 = nextTrack.find((u"INDEX", u"01"))
47 | return msfToMs(index1[0][2]) - msfToMs(index0[0][2])
48 | except IndexError:
49 | return 0
50 |
51 | def getField(self, prefix):
52 | try:
53 | return self.find(prefix)[0][len(prefix)]
54 | except IndexError:
55 | return u""
56 |
57 | def getArtist(self):
58 | return self.getField((u"PERFORMER",))
59 |
60 | def getTitle(self):
61 | return self.getField((u"TITLE",))
62 |
63 | def setArtist(self, artist):
64 | found = False
65 | for item in self:
66 | if item[0] == u"PERFORMER":
67 | if not found:
68 | item[1] = artist
69 | found = True
70 | else:
71 | del item
72 | if not found:
73 | self.append((u"PERFORMER", artist))
74 |
75 | artist = property(getArtist, setArtist)
76 |
77 |
78 | class Cuesheet(object):
79 |
80 | def __init__(self, filename):
81 | self.filename = filename
82 | self.tracks = []
83 |
84 | def read(self):
85 | with open(encode_filename(self.filename)) as f:
86 | self.parse(f.readlines())
87 |
88 | def unquote(self, string):
89 | if string.startswith('"'):
90 | if string.endswith('"'):
91 | return string[1:-1]
92 | else:
93 | return string[1:]
94 | return string
95 |
96 | def quote(self, string):
97 | if _whitespace_re.search(string):
98 | return '"' + string.replace('"', '\'') + '"'
99 | return string
100 |
101 | def parse(self, lines):
102 | track = CuesheetTrack(self, 0)
103 | self.tracks = [track]
104 | isUnicode = False
105 | for line in lines:
106 | # remove BOM
107 | if line.startswith('\xfe\xff'):
108 | isUnicode = True
109 | line = line[1:]
110 | # decode to unicode string
111 | line = line.strip()
112 | if isUnicode:
113 | line = line.decode('UTF-8', 'replace')
114 | else:
115 | line = line.decode('ISO-8859-1', 'replace')
116 | # parse the line
117 | split = [self.unquote(s) for s in _split_re.findall(line)]
118 | keyword = split[0].upper()
119 | if keyword == 'TRACK':
120 | trackNum = int(split[1])
121 | track = CuesheetTrack(self, trackNum)
122 | self.tracks.append(track)
123 | track.append(split)
124 |
125 | def write(self):
126 | lines = []
127 | for track in self.tracks:
128 | num = track.index
129 | for line in track:
130 | indent = 0
131 | if num > 0:
132 | if line[0] == "TRACK":
133 | indent = 2
134 | elif line[0] != "FILE":
135 | indent = 4
136 | line2 = u" ".join([self.quote(s) for s in line])
137 | lines.append(" " * indent + line2.encode("UTF-8") + "\n")
138 | with open(encode_filename(self.filename), "wt") as f:
139 | f.writelines(lines)
140 |
141 |
142 | class GenerateCuesheet(BaseAction):
143 | NAME = "Generate &Cuesheet..."
144 |
145 | def callback(self, objs):
146 | album = objs[0]
147 | current_directory = self.config.persist["current_directory"] or QtCore.QDir.homePath()
148 | current_directory = find_existing_path(unicode(current_directory))
149 | filename, selected_format = QtGui.QFileDialog.getSaveFileNameAndFilter(
150 | None, "", current_directory, "Cuesheet (*.cue)")
151 | if filename:
152 | filename = unicode(filename)
153 | cuesheet = Cuesheet(filename)
154 | #try: cuesheet.read()
155 | #except IOError: pass
156 | while len(cuesheet.tracks) <= len(album.tracks):
157 | track = CuesheetTrack(cuesheet, len(cuesheet.tracks))
158 | cuesheet.tracks.append(track)
159 | #if len(cuesheet.tracks) > len(album.tracks) - 1:
160 | # cuesheet.tracks = cuesheet.tracks[0:len(album.tracks)+1]
161 |
162 | t = cuesheet.tracks[0]
163 | t.set("PERFORMER", album.metadata["albumartist"])
164 | t.set("TITLE", album.metadata["album"])
165 | t.set("REM", "MUSICBRAINZ_ALBUM_ID", album.metadata["musicbrainz_albumid"])
166 | t.set("REM", "MUSICBRAINZ_ALBUM_ARTIST_ID", album.metadata["musicbrainz_albumartistid"])
167 | if "date" in album.metadata:
168 | t.set("REM", "DATE", album.metadata["date"])
169 | index = 0.0
170 | for i, track in enumerate(album.tracks):
171 | mm = index / 60.0
172 | ss = (mm - int(mm)) * 60.0
173 | ff = (ss - int(ss)) * 75.0
174 | index += track.metadata.length / 1000.0
175 | t = cuesheet.tracks[i + 1]
176 | t.set("TRACK", "%02d" % (i + 1), "AUDIO")
177 | t.set("PERFORMER", track.metadata["artist"])
178 | t.set("TITLE", track.metadata["title"])
179 | t.set("REM", "MUSICBRAINZ_TRACK_ID", track.metadata["musicbrainz_trackid"])
180 | t.set("REM", "MUSICBRAINZ_ARTIST_ID", track.metadata["musicbrainz_artistid"])
181 | t.set("INDEX", "01", "%02d:%02d:%02d" % (mm, ss, ff))
182 | for file in track.linked_files:
183 | audio_filename = file.filename
184 | if os.path.dirname(filename) == os.path.dirname(audio_filename):
185 | audio_filename = os.path.basename(audio_filename)
186 | cuesheet.tracks[i].set("FILE", audio_filename, "MP3")
187 |
188 | cuesheet.write()
189 |
190 |
191 | action = GenerateCuesheet()
192 | register_album_action(action)
193 |
--------------------------------------------------------------------------------
/plugins/decode_cyrillic/decode_cyrillic.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # This is the Decode Cyrillic plugin for MusicBrainz Picard.
4 | # Copyright (C) 2015 aeontech
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in
14 | # all copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 |
24 | from __future__ import print_function
25 | PLUGIN_NAME = u"Decode Cyrillic"
26 | PLUGIN_AUTHOR = u"aeontech"
27 | PLUGIN_DESCRIPTION = u'''
28 | This plugin helps you quickly convert mis-encoded cyrillic Windows-1251 tags
29 | to proper UTF-8 encoded strings. If your track/album names look something like
30 | "Àëèñà â ñò›àíå ÷óäåñ", run this plugin from the context menu
31 | before running the "Lookup" or "Scan" tools
32 | '''
33 | PLUGIN_VERSION = "1.0"
34 | PLUGIN_API_VERSIONS = ["1.0"]
35 | PLUGIN_LICENSE = "MIT"
36 | PLUGIN_LICENSE_URL = "https://opensource.org/licenses/MIT"
37 |
38 | from picard import log
39 | from picard.cluster import Cluster
40 | from picard.ui.itemviews import BaseAction, register_cluster_action
41 |
42 | _decode_tags = [
43 | 'title',
44 | 'albumartist',
45 | 'artist',
46 | 'album',
47 | 'artistsort'
48 | ]
49 | # _from_encoding = "latin1"
50 | # _to_encoding = "cp1251"
51 |
52 |
53 | # TODO:
54 | # - extend to support multiple codepage decoding, not just cp1251->latin1
55 | # instead, try the common variations, and show a dialog to the user,
56 | # allowing him to select the correct transcoding. See 2cyr.com for example.
57 | # - also see http://stackoverflow.com/questions/23326531/how-to-decode-cp1252-string
58 |
59 | class DecodeCyrillic(BaseAction):
60 | NAME = "Unmangle cyrillic metadata"
61 |
62 | def unmangle(self, tag, value):
63 | try:
64 | unmangled_value = value.encode('latin1').decode('cp1251')
65 | except UnicodeEncodeError:
66 | unmangled_value = value
67 | log.debug("%s: could not unmangle tag %s; original value: %s" % (PLUGIN_NAME, tag, value))
68 | return unmangled_value
69 |
70 | def callback(self, objs):
71 | for cluster in objs:
72 | if not isinstance(cluster, Cluster):
73 | continue
74 |
75 | for tag in _decode_tags:
76 | if not (tag in cluster.metadata):
77 | continue
78 |
79 | cluster.metadata[tag] = self.unmangle(tag, cluster.metadata[tag])
80 |
81 | log.debug("cluster name is %s by %s" % (cluster.metadata['album'], cluster.metadata['albumartist']))
82 |
83 | for i, file in enumerate(cluster.files):
84 |
85 | log.debug("%s: Trying to unmangle file - original metadata %s" % (PLUGIN_NAME, file.orig_metadata))
86 |
87 | for tag in _decode_tags:
88 |
89 | if not (tag in file.metadata):
90 | continue
91 |
92 | unmangled_tag = self.unmangle(tag, file.metadata[tag])
93 |
94 | file.orig_metadata[tag] = unmangled_tag
95 | file.metadata[tag] = unmangled_tag
96 |
97 | file.orig_metadata.changed = True
98 | file.metadata.changed = True
99 | file.update(signal=True)
100 |
101 | cluster.update()
102 |
103 | register_cluster_action(DecodeCyrillic())
104 |
--------------------------------------------------------------------------------
/plugins/discnumber/discnumber.py:
--------------------------------------------------------------------------------
1 | PLUGIN_NAME = 'Disc Numbers'
2 | PLUGIN_AUTHOR = 'Lukas Lalinsky'
3 | PLUGIN_DESCRIPTION = '''Moves disc numbers and subtitles from album titles to separate tags. For example:
4 | "Aerial (disc 1: A Sea of Honey)"
5 |
6 | album = "Aerial"
7 | discnumber = "1"
8 | discsubtitle = "A Sea of Honey"
9 | '''
10 | PLUGIN_VERSION = "0.1"
11 | PLUGIN_API_VERSIONS = ["0.9.0", "0.10", "0.15"]
12 |
13 | from picard.metadata import register_album_metadata_processor
14 | import re
15 |
16 | _discnumber_re = re.compile(r"\s+\(disc (\d+)(?::\s+([^)]+))?\)")
17 |
18 |
19 | def remove_discnumbers(tagger, metadata, release):
20 | matches = _discnumber_re.search(metadata["album"])
21 | if matches:
22 | metadata["discnumber"] = matches.group(1)
23 | if matches.group(2):
24 | metadata["discsubtitle"] = matches.group(2)
25 | metadata["album"] = _discnumber_re.sub('', metadata["album"])
26 |
27 | register_album_metadata_processor(remove_discnumbers)
28 |
--------------------------------------------------------------------------------
/plugins/fanarttv/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2015 Philipp Wolfer
4 | #
5 | # This program is free software; you can redistribute it and/or
6 | # modify it under the terms of the GNU General Public License
7 | # as published by the Free Software Foundation; either version 2
8 | # of the License, or (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program; if not, write to the Free Software
17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 | # 02110-1301, USA.
19 |
20 | PLUGIN_NAME = u'fanart.tv cover art'
21 | PLUGIN_AUTHOR = u'Philipp Wolfer'
22 | PLUGIN_DESCRIPTION = u'Use cover art from fanart.tv. To use this plugin you have to register a personal API key on https://fanart.tv/get-an-api-key/'
23 | PLUGIN_VERSION = "0.5"
24 | PLUGIN_API_VERSIONS = ["1.4.0"]
25 | PLUGIN_LICENSE = "GPL-2.0"
26 | PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"
27 |
28 | import json
29 | import traceback
30 | from PyQt4.QtCore import QUrl
31 | from PyQt4.QtNetwork import QNetworkReply
32 | from picard import config, log
33 | from picard.coverart.providers import CoverArtProvider, register_cover_art_provider
34 | from picard.coverart.image import CoverArtImage
35 | from picard.util import partial
36 | from picard.ui.options import register_options_page, OptionsPage
37 | from picard.config import TextOption, BoolOption
38 | from picard.plugins.fanarttv.ui_options_fanarttv import Ui_FanartTvOptionsPage
39 |
40 | FANART_HOST = "webservice.fanart.tv"
41 | FANART_PORT = 80
42 | FANART_APIKEY = "21305dd1589766f4d544535ad4df12f4"
43 |
44 | OPTION_CDART_ALWAYS = "always"
45 | OPTION_CDART_NEVER = "never"
46 | OPTION_CDART_NOALBUMART = "noalbumart"
47 |
48 |
49 | def cover_sort_key(cover):
50 | """For sorting a list of cover arts by likes."""
51 | try:
52 | return int(cover["likes"]) if "likes" in cover else 0
53 | except ValueError:
54 | return 0
55 |
56 |
57 | class FanartTvCoverArtImage(CoverArtImage):
58 |
59 | """Image from Cover Art Archive"""
60 |
61 | support_types = True
62 | sourceprefix = u"FATV"
63 |
64 |
65 | class CoverArtProviderFanartTv(CoverArtProvider):
66 |
67 | """Use fanart.tv to get cover art"""
68 |
69 | NAME = "fanart.tv"
70 |
71 | def enabled(self):
72 | return self._client_key != "" and \
73 | super(CoverArtProviderFanartTv, self).enabled() and \
74 | not self.coverart.front_image_found
75 |
76 | def queue_downloads(self):
77 | release_group_id = self.metadata["musicbrainz_releasegroupid"]
78 | path = "/v3/music/albums/%s" % \
79 | (release_group_id, )
80 | queryargs = {"api_key": QUrl.toPercentEncoding(FANART_APIKEY),
81 | "client_key": QUrl.toPercentEncoding(self._client_key),
82 | }
83 | log.debug("CoverArtProviderFanartTv.queue_downloads: %s" % path)
84 | self.album.tagger.xmlws.download(
85 | FANART_HOST,
86 | FANART_PORT,
87 | path,
88 | partial(self._json_downloaded, release_group_id),
89 | priority=True,
90 | important=False,
91 | queryargs=queryargs)
92 | self.album._requests += 1
93 | return CoverArtProvider.WAIT
94 |
95 | @property
96 | def _client_key(self):
97 | return config.setting["fanarttv_client_key"]
98 |
99 | def _json_downloaded(self, release_group_id, data, reply, error):
100 | self.album._requests -= 1
101 |
102 | if error:
103 | if error != QNetworkReply.ContentNotFoundError:
104 | error_level = log.error
105 | else:
106 | error_level = log.debug
107 | error_level("Problem requesting metadata in fanart.tv plugin: %s", error)
108 | else:
109 | try:
110 | response = json.loads(data)
111 | release = response["albums"][release_group_id]
112 |
113 | if "albumcover" in release:
114 | covers = release["albumcover"]
115 | types = ["front"]
116 | self._select_and_add_cover_art(covers, types)
117 |
118 | if "cdart" in release and \
119 | (config.setting["fanarttv_use_cdart"] == OPTION_CDART_ALWAYS
120 | or (config.setting["fanarttv_use_cdart"] == OPTION_CDART_NOALBUMART
121 | and "albumcover" not in release)):
122 | covers = release["cdart"]
123 | types = ["medium"]
124 | if not "albumcover" in release:
125 | types.append("front")
126 | self._select_and_add_cover_art(covers, types)
127 | except:
128 | log.error("Problem processing downloaded metadata in fanart.tv plugin: %s", traceback.format_exc())
129 |
130 | self.next_in_queue()
131 |
132 | def _select_and_add_cover_art(self, covers, types):
133 | covers = sorted(covers, key=cover_sort_key, reverse=True)
134 | url = covers[0]["url"]
135 | log.debug("CoverArtProviderFanartTv found artwork %s" % url)
136 | self.queue_put(FanartTvCoverArtImage(url, types=types))
137 |
138 |
139 | class FanartTvOptionsPage(OptionsPage):
140 |
141 | NAME = "fanarttv"
142 | TITLE = "fanart.tv"
143 | PARENT = "plugins"
144 |
145 | options = [
146 | TextOption("setting", "fanarttv_client_key", ""),
147 | TextOption("setting", "fanarttv_use_cdart", OPTION_CDART_NOALBUMART),
148 | ]
149 |
150 | def __init__(self, parent=None):
151 | super(FanartTvOptionsPage, self).__init__(parent)
152 | self.ui = Ui_FanartTvOptionsPage()
153 | self.ui.setupUi(self)
154 |
155 | def load(self):
156 | self.ui.fanarttv_client_key.setText(config.setting["fanarttv_client_key"])
157 | if config.setting["fanarttv_use_cdart"] == OPTION_CDART_ALWAYS:
158 | self.ui.fanarttv_cdart_use_always.setChecked(True)
159 | elif config.setting["fanarttv_use_cdart"] == OPTION_CDART_NEVER:
160 | self.ui.fanarttv_cdart_use_never.setChecked(True)
161 | elif config.setting["fanarttv_use_cdart"] == OPTION_CDART_NOALBUMART:
162 | self.ui.fanarttv_cdart_use_if_no_albumcover.setChecked(True)
163 |
164 | def save(self):
165 | config.setting["fanarttv_client_key"] = unicode(self.ui.fanarttv_client_key.text())
166 | if self.ui.fanarttv_cdart_use_always.isChecked():
167 | config.setting["fanarttv_use_cdart"] = OPTION_CDART_ALWAYS
168 | elif self.ui.fanarttv_cdart_use_never.isChecked():
169 | config.setting["fanarttv_use_cdart"] = OPTION_CDART_NEVER
170 | elif self.ui.fanarttv_cdart_use_if_no_albumcover.isChecked():
171 | config.setting["fanarttv_use_cdart"] = OPTION_CDART_NOALBUMART
172 |
173 |
174 | register_cover_art_provider(CoverArtProviderFanartTv)
175 | register_options_page(FanartTvOptionsPage)
176 |
--------------------------------------------------------------------------------
/plugins/fanarttv/options_fanarttv.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | FanartTvOptionsPage
4 |
5 |
6 |
7 | 0
8 | 0
9 | 340
10 | 317
11 |
12 |
13 |
14 |
15 | 6
16 |
17 |
18 | 9
19 |
20 |
21 | 9
22 |
23 |
24 | 9
25 |
26 |
27 | 9
28 |
29 | -
30 |
31 |
32 | fanart.tv cover art
33 |
34 |
35 |
36 | 2
37 |
38 |
39 | 9
40 |
41 |
42 | 9
43 |
44 |
45 | 9
46 |
47 |
48 | 9
49 |
50 | -
51 |
52 |
53 | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
54 | <html><head><meta name="qrichtext" content="1" /><style type="text/css">
55 | p, li { white-space: pre-wrap; }
56 | </style></head><body style=" font-family:'Cantarell'; font-size:10pt; font-weight:400; font-style:normal;">
57 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">This plugin loads cover art from <a href="http://fanart.tv/"><span style=" text-decoration: underline; color:#0000ff;">fanart.tv</span></a>. If you want to improve the results of this plugin please contribute.</p>
58 | <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p>
59 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">In order to use this plugin you have to register a personal API key on<br /><a href="https://fanart.tv/get-an-api-key/"><span style=" text-decoration: underline; color:#0000ff;">https://fanart.tv/get-an-api-key/</span></a></p></body></html>
60 |
61 |
62 | Qt::RichText
63 |
64 |
65 | true
66 |
67 |
68 | true
69 |
70 |
71 |
72 | -
73 |
74 |
75 | Qt::Vertical
76 |
77 |
78 | QSizePolicy::Expanding
79 |
80 |
81 |
82 | 20
83 | 40
84 |
85 |
86 |
87 |
88 | -
89 |
90 |
91 | Enter your personal API key here:
92 |
93 |
94 |
95 | -
96 |
97 |
98 |
99 |
100 |
101 | -
102 |
103 |
104 | Medium images
105 |
106 |
107 | -
108 |
109 |
110 | Always load medium images
111 |
112 |
113 |
114 | -
115 |
116 |
117 | Load only if no front cover is available
118 |
119 |
120 |
121 | -
122 |
123 |
124 | Never load medium images
125 |
126 |
127 |
128 |
129 |
130 |
131 | -
132 |
133 |
134 | Qt::Vertical
135 |
136 |
137 |
138 | 20
139 | 40
140 |
141 |
142 |
143 |
144 | -
145 |
146 |
147 | Qt::Vertical
148 |
149 |
150 |
151 | 20
152 | 40
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
--------------------------------------------------------------------------------
/plugins/fanarttv/ui_options_fanarttv.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Automatically generated - don't edit.
4 | # Use `python setup.py build_ui` to update it.
5 |
6 | from PyQt4 import QtCore, QtGui
7 |
8 | try:
9 | _fromUtf8 = QtCore.QString.fromUtf8
10 | except AttributeError:
11 | def _fromUtf8(s):
12 | return s
13 |
14 | try:
15 | _encoding = QtGui.QApplication.UnicodeUTF8
16 | def _translate(context, text, disambig):
17 | return QtGui.QApplication.translate(context, text, disambig, _encoding)
18 | except AttributeError:
19 | def _translate(context, text, disambig):
20 | return QtGui.QApplication.translate(context, text, disambig)
21 |
22 | class Ui_FanartTvOptionsPage(object):
23 | def setupUi(self, FanartTvOptionsPage):
24 | FanartTvOptionsPage.setObjectName(_fromUtf8("FanartTvOptionsPage"))
25 | FanartTvOptionsPage.resize(340, 317)
26 | self.vboxlayout = QtGui.QVBoxLayout(FanartTvOptionsPage)
27 | self.vboxlayout.setSpacing(6)
28 | self.vboxlayout.setMargin(9)
29 | self.vboxlayout.setObjectName(_fromUtf8("vboxlayout"))
30 | self.groupBox = QtGui.QGroupBox(FanartTvOptionsPage)
31 | self.groupBox.setObjectName(_fromUtf8("groupBox"))
32 | self.vboxlayout1 = QtGui.QVBoxLayout(self.groupBox)
33 | self.vboxlayout1.setSpacing(2)
34 | self.vboxlayout1.setMargin(9)
35 | self.vboxlayout1.setObjectName(_fromUtf8("vboxlayout1"))
36 | self.label = QtGui.QLabel(self.groupBox)
37 | self.label.setTextFormat(QtCore.Qt.RichText)
38 | self.label.setWordWrap(True)
39 | self.label.setOpenExternalLinks(True)
40 | self.label.setObjectName(_fromUtf8("label"))
41 | self.vboxlayout1.addWidget(self.label)
42 | spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
43 | self.vboxlayout1.addItem(spacerItem)
44 | self.label_2 = QtGui.QLabel(self.groupBox)
45 | self.label_2.setObjectName(_fromUtf8("label_2"))
46 | self.vboxlayout1.addWidget(self.label_2)
47 | self.fanarttv_client_key = QtGui.QLineEdit(self.groupBox)
48 | self.fanarttv_client_key.setObjectName(_fromUtf8("fanarttv_client_key"))
49 | self.vboxlayout1.addWidget(self.fanarttv_client_key)
50 | self.vboxlayout.addWidget(self.groupBox)
51 | self.verticalGroupBox = QtGui.QGroupBox(FanartTvOptionsPage)
52 | self.verticalGroupBox.setObjectName(_fromUtf8("verticalGroupBox"))
53 | self.verticalLayout = QtGui.QVBoxLayout(self.verticalGroupBox)
54 | self.verticalLayout.setObjectName(_fromUtf8("verticalLayout"))
55 | self.fanarttv_cdart_use_always = QtGui.QRadioButton(self.verticalGroupBox)
56 | self.fanarttv_cdart_use_always.setObjectName(_fromUtf8("fanarttv_cdart_use_always"))
57 | self.verticalLayout.addWidget(self.fanarttv_cdart_use_always)
58 | self.fanarttv_cdart_use_if_no_albumcover = QtGui.QRadioButton(self.verticalGroupBox)
59 | self.fanarttv_cdart_use_if_no_albumcover.setObjectName(_fromUtf8("fanarttv_cdart_use_if_no_albumcover"))
60 | self.verticalLayout.addWidget(self.fanarttv_cdart_use_if_no_albumcover)
61 | self.fanarttv_cdart_use_never = QtGui.QRadioButton(self.verticalGroupBox)
62 | self.fanarttv_cdart_use_never.setObjectName(_fromUtf8("fanarttv_cdart_use_never"))
63 | self.verticalLayout.addWidget(self.fanarttv_cdart_use_never)
64 | self.vboxlayout.addWidget(self.verticalGroupBox)
65 | spacerItem1 = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
66 | self.vboxlayout.addItem(spacerItem1)
67 | spacerItem2 = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
68 | self.vboxlayout.addItem(spacerItem2)
69 |
70 | self.retranslateUi(FanartTvOptionsPage)
71 | QtCore.QMetaObject.connectSlotsByName(FanartTvOptionsPage)
72 |
73 | def retranslateUi(self, FanartTvOptionsPage):
74 | self.groupBox.setTitle(_("fanart.tv cover art"))
75 | self.label.setText(_translate("FanartTvOptionsPage", "\n"
76 | " \n"
79 | "This plugin loads cover art from fanart.tv . If you want to improve the results of this plugin please contribute.
\n"
80 | "
\n"
81 | "In order to use this plugin you have to register a personal API key onhttps://fanart.tv/get-an-api-key/
", None))
82 | self.label_2.setText(_("Enter your personal API key here:"))
83 | self.verticalGroupBox.setTitle(_("Medium images"))
84 | self.fanarttv_cdart_use_always.setText(_("Always load medium images"))
85 | self.fanarttv_cdart_use_if_no_albumcover.setText(_("Load only if no front cover is available"))
86 | self.fanarttv_cdart_use_never.setText(_("Never load medium images"))
87 |
88 |
--------------------------------------------------------------------------------
/plugins/featartist/featartist.py:
--------------------------------------------------------------------------------
1 | PLUGIN_NAME = 'Feat. Artists Removed'
2 | PLUGIN_AUTHOR = 'Lukas Lalinsky, Bryan Toth'
3 | PLUGIN_DESCRIPTION = 'Removes feat. artists from track titles. Substitution is case insensitive.'
4 | PLUGIN_VERSION = "0.3"
5 | PLUGIN_API_VERSIONS = ["0.9.0", "0.10", "0.15", "0.16"]
6 |
7 | from picard.metadata import register_track_metadata_processor
8 | import re
9 |
10 | _feat_re = re.compile(r"\s+\(feat\. [^)]*\)", re.IGNORECASE)
11 |
12 |
13 | def remove_featartists(tagger, metadata, release, track):
14 | metadata["title"] = _feat_re.sub("", metadata["title"])
15 |
16 | register_track_metadata_processor(remove_featartists)
17 |
--------------------------------------------------------------------------------
/plugins/featartistsintitles/featartistsintitles.py:
--------------------------------------------------------------------------------
1 | PLUGIN_NAME = 'Feat. Artists in Titles'
2 | PLUGIN_AUTHOR = 'Lukas Lalinsky, Michael Wiencek, Bryan Toth, JeromyNix (NobahdiAtoll)'
3 | PLUGIN_DESCRIPTION = 'Move "feat." from artist names to album and track titles. Match is case insensitive.'
4 | PLUGIN_VERSION = "0.4"
5 | PLUGIN_API_VERSIONS = ["0.9.0", "0.10", "0.15", "0.16"]
6 |
7 | from picard.metadata import register_album_metadata_processor, register_track_metadata_processor
8 | import re
9 |
10 | _feat_re = re.compile(r"([\s\S]+) feat\.([\s\S]+)", re.IGNORECASE)
11 |
12 |
13 | def move_album_featartists(tagger, metadata, release):
14 | match = _feat_re.match(metadata["albumartist"])
15 | if match:
16 | metadata["albumartist"] = match.group(1)
17 | metadata["album"] += " (feat.%s)" % match.group(2)
18 | match = _feat_re.match(metadata["albumartistsort"])
19 | if match:
20 | metadata["albumartistsort"] = match.group(1)
21 |
22 |
23 | def move_track_featartists(tagger, metadata, release, track):
24 | match = _feat_re.match(metadata["artist"])
25 | if match:
26 | metadata["artist"] = match.group(1)
27 | metadata["title"] += " (feat.%s)" % match.group(2)
28 | match = _feat_re.match(metadata["artistsort"])
29 | if match:
30 | metadata["artistsort"] = match.group(1)
31 |
32 | register_album_metadata_processor(move_album_featartists)
33 | register_track_metadata_processor(move_track_featartists)
34 |
--------------------------------------------------------------------------------
/plugins/keep/keep.py:
--------------------------------------------------------------------------------
1 | PLUGIN_NAME = "Keep tags"
2 | PLUGIN_AUTHOR = "Wieland Hoffmann"
3 | PLUGIN_DESCRIPTION = """
4 | Adds a $keep() function to delete all tags except the ones that you want.
5 | Tags beginning with `musicbrainz_` are kept automatically, as are tags
6 | beginning with `_`."""
7 |
8 | PLUGIN_VERSION = "1.1"
9 | PLUGIN_API_VERSIONS = ["0.15.0", "0.15.1", "0.16.0", "1.0.0", "1.1.0", "1.2.0",
10 | "1.3.0", "2.0"]
11 | PLUGIN_LICENSE = "GPL-2.0 or later"
12 | PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"
13 |
14 | from picard.script import register_script_function
15 |
16 |
17 | def transltag(tag):
18 | if tag.startswith("~"):
19 | return "_" + tag[1:]
20 | return tag
21 |
22 |
23 | @register_script_function
24 | def keep(parser, *keeptags):
25 | tags = list(parser.context.keys())
26 | for tag in tags:
27 | if (transltag(tag) not in keeptags and
28 | not tag.startswith("musicbrainz_") and
29 | not tag[0] == "~"):
30 | parser.context.pop(tag, None)
31 | return ""
32 |
--------------------------------------------------------------------------------
/plugins/lastfm/options_lastfm.ui:
--------------------------------------------------------------------------------
1 |
2 | LastfmOptionsPage
3 |
4 |
5 |
6 | 0
7 | 0
8 | 305
9 | 317
10 |
11 |
12 |
13 |
14 | 9
15 |
16 |
17 | 6
18 |
19 | -
20 |
21 |
22 | Last.fm
23 |
24 |
25 |
26 | 9
27 |
28 |
29 | 2
30 |
31 | -
32 |
33 |
34 | Use track tags
35 |
36 |
37 |
38 | -
39 |
40 |
41 | Use artist tags
42 |
43 |
44 |
45 |
46 |
47 |
48 | -
49 |
50 |
51 | Tags
52 |
53 |
54 |
55 | 9
56 |
57 |
58 | 2
59 |
60 | -
61 |
62 |
63 | Ignore tags:
64 |
65 |
66 |
67 | -
68 |
69 |
70 | -
71 |
72 |
73 | 0
74 |
75 |
76 | 6
77 |
78 | -
79 |
80 |
81 |
82 | 5
83 | 5
84 | 4
85 | 0
86 |
87 |
88 |
89 | Join multiple tags with:
90 |
91 |
92 |
93 | -
94 |
95 |
96 |
97 | 5
98 | 0
99 | 1
100 | 0
101 |
102 |
103 |
104 | true
105 |
106 | -
107 |
108 |
109 |
110 |
111 | -
112 |
113 | /
114 |
115 |
116 | -
117 |
118 | ,
119 |
120 |
121 |
122 |
123 |
124 |
125 | -
126 |
127 |
128 | 0
129 |
130 |
131 | 6
132 |
133 | -
134 |
135 |
136 |
137 | 7
138 | 5
139 | 0
140 | 0
141 |
142 |
143 |
144 | Minimal tag usage:
145 |
146 |
147 | min_tag_usage
148 |
149 |
150 |
151 | -
152 |
153 |
154 | %
155 |
156 |
157 | 100
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 | -
167 |
168 |
169 | Qt::Vertical
170 |
171 |
172 |
173 | 263
174 | 21
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 | use_track_tags
183 | ignore_tags
184 |
185 |
186 |
187 |
188 |
--------------------------------------------------------------------------------
/plugins/lastfm/ui_options_lastfm.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Automatically generated - don't edit.
4 | # Use `python setup.py build_ui` to update it.
5 |
6 | from PyQt4 import QtCore, QtGui
7 |
8 | try:
9 | _fromUtf8 = QtCore.QString.fromUtf8
10 | except AttributeError:
11 | _fromUtf8 = lambda s: s
12 |
13 | class Ui_LastfmOptionsPage(object):
14 | def setupUi(self, LastfmOptionsPage):
15 | LastfmOptionsPage.setObjectName(_fromUtf8("LastfmOptionsPage"))
16 | LastfmOptionsPage.resize(305, 317)
17 | self.vboxlayout = QtGui.QVBoxLayout(LastfmOptionsPage)
18 | self.vboxlayout.setMargin(9)
19 | self.vboxlayout.setSpacing(6)
20 | self.vboxlayout.setObjectName(_fromUtf8("vboxlayout"))
21 | self.rename_files = QtGui.QGroupBox(LastfmOptionsPage)
22 | self.rename_files.setObjectName(_fromUtf8("rename_files"))
23 | self.vboxlayout1 = QtGui.QVBoxLayout(self.rename_files)
24 | self.vboxlayout1.setMargin(9)
25 | self.vboxlayout1.setSpacing(2)
26 | self.vboxlayout1.setObjectName(_fromUtf8("vboxlayout1"))
27 | self.use_track_tags = QtGui.QCheckBox(self.rename_files)
28 | self.use_track_tags.setObjectName(_fromUtf8("use_track_tags"))
29 | self.vboxlayout1.addWidget(self.use_track_tags)
30 | self.use_artist_tags = QtGui.QCheckBox(self.rename_files)
31 | self.use_artist_tags.setObjectName(_fromUtf8("use_artist_tags"))
32 | self.vboxlayout1.addWidget(self.use_artist_tags)
33 | self.vboxlayout.addWidget(self.rename_files)
34 | self.rename_files_2 = QtGui.QGroupBox(LastfmOptionsPage)
35 | self.rename_files_2.setObjectName(_fromUtf8("rename_files_2"))
36 | self.vboxlayout2 = QtGui.QVBoxLayout(self.rename_files_2)
37 | self.vboxlayout2.setMargin(9)
38 | self.vboxlayout2.setSpacing(2)
39 | self.vboxlayout2.setObjectName(_fromUtf8("vboxlayout2"))
40 | self.ignore_tags_2 = QtGui.QLabel(self.rename_files_2)
41 | self.ignore_tags_2.setObjectName(_fromUtf8("ignore_tags_2"))
42 | self.vboxlayout2.addWidget(self.ignore_tags_2)
43 | self.ignore_tags = QtGui.QLineEdit(self.rename_files_2)
44 | self.ignore_tags.setObjectName(_fromUtf8("ignore_tags"))
45 | self.vboxlayout2.addWidget(self.ignore_tags)
46 | self.hboxlayout = QtGui.QHBoxLayout()
47 | self.hboxlayout.setMargin(0)
48 | self.hboxlayout.setSpacing(6)
49 | self.hboxlayout.setObjectName(_fromUtf8("hboxlayout"))
50 | self.ignore_tags_4 = QtGui.QLabel(self.rename_files_2)
51 | sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Policy(5), QtGui.QSizePolicy.Policy(5))
52 | sizePolicy.setHorizontalStretch(4)
53 | sizePolicy.setVerticalStretch(0)
54 | sizePolicy.setHeightForWidth(self.ignore_tags_4.sizePolicy().hasHeightForWidth())
55 | self.ignore_tags_4.setSizePolicy(sizePolicy)
56 | self.ignore_tags_4.setObjectName(_fromUtf8("ignore_tags_4"))
57 | self.hboxlayout.addWidget(self.ignore_tags_4)
58 | self.join_tags = QtGui.QComboBox(self.rename_files_2)
59 | sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Policy(5), QtGui.QSizePolicy.Policy(0))
60 | sizePolicy.setHorizontalStretch(1)
61 | sizePolicy.setVerticalStretch(0)
62 | sizePolicy.setHeightForWidth(self.join_tags.sizePolicy().hasHeightForWidth())
63 | self.join_tags.setSizePolicy(sizePolicy)
64 | self.join_tags.setEditable(True)
65 | self.join_tags.setObjectName(_fromUtf8("join_tags"))
66 | self.join_tags.addItem(_fromUtf8(""))
67 | self.join_tags.setItemText(0, _fromUtf8(""))
68 | self.join_tags.addItem(_fromUtf8(""))
69 | self.join_tags.addItem(_fromUtf8(""))
70 | self.hboxlayout.addWidget(self.join_tags)
71 | self.vboxlayout2.addLayout(self.hboxlayout)
72 | self.hboxlayout1 = QtGui.QHBoxLayout()
73 | self.hboxlayout1.setMargin(0)
74 | self.hboxlayout1.setSpacing(6)
75 | self.hboxlayout1.setObjectName(_fromUtf8("hboxlayout1"))
76 | self.label_4 = QtGui.QLabel(self.rename_files_2)
77 | sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Policy(7), QtGui.QSizePolicy.Policy(5))
78 | sizePolicy.setHorizontalStretch(0)
79 | sizePolicy.setVerticalStretch(0)
80 | sizePolicy.setHeightForWidth(self.label_4.sizePolicy().hasHeightForWidth())
81 | self.label_4.setSizePolicy(sizePolicy)
82 | self.label_4.setObjectName(_fromUtf8("label_4"))
83 | self.hboxlayout1.addWidget(self.label_4)
84 | self.min_tag_usage = QtGui.QSpinBox(self.rename_files_2)
85 | self.min_tag_usage.setMaximum(100)
86 | self.min_tag_usage.setObjectName(_fromUtf8("min_tag_usage"))
87 | self.hboxlayout1.addWidget(self.min_tag_usage)
88 | self.vboxlayout2.addLayout(self.hboxlayout1)
89 | self.vboxlayout.addWidget(self.rename_files_2)
90 | spacerItem = QtGui.QSpacerItem(263, 21, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
91 | self.vboxlayout.addItem(spacerItem)
92 | self.label_4.setBuddy(self.min_tag_usage)
93 |
94 | self.retranslateUi(LastfmOptionsPage)
95 | QtCore.QMetaObject.connectSlotsByName(LastfmOptionsPage)
96 | LastfmOptionsPage.setTabOrder(self.use_track_tags, self.ignore_tags)
97 |
98 | def retranslateUi(self, LastfmOptionsPage):
99 | self.rename_files.setTitle(_("Last.fm"))
100 | self.use_track_tags.setText(_("Use track tags"))
101 | self.use_artist_tags.setText(_("Use artist tags"))
102 | self.rename_files_2.setTitle(_("Tags"))
103 | self.ignore_tags_2.setText(_("Ignore tags:"))
104 | self.ignore_tags_4.setText(_("Join multiple tags with:"))
105 | self.join_tags.setItemText(1, _(" / "))
106 | self.join_tags.setItemText(2, _(", "))
107 | self.label_4.setText(_("Minimal tag usage:"))
108 | self.min_tag_usage.setSuffix(_(" %"))
109 |
110 |
--------------------------------------------------------------------------------
/plugins/loadasnat/loadasnat.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | PLUGIN_NAME = u"Load as non-album track"
4 | PLUGIN_AUTHOR = u"Philipp Wolfer"
5 | PLUGIN_DESCRIPTION = "Allows loading selected tracks as non-album tracks. Useful for tagging single tracks where you do not care about the album."
6 | PLUGIN_VERSION = "0.1"
7 | PLUGIN_API_VERSIONS = ["1.4.0"]
8 | PLUGIN_LICENSE = "GPL-2.0"
9 | PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"
10 |
11 | from picard import log
12 | from picard.album import Track
13 | from picard.ui.itemviews import BaseAction, register_track_action
14 |
15 | class LoadAsNat(BaseAction):
16 | NAME = "Load as non-album track..."
17 |
18 | def callback(self, objs):
19 | tracks = [t for t in objs if isinstance(t, Track)]
20 |
21 | if len(tracks) == 0:
22 | return
23 |
24 | for track in tracks:
25 | nat = self.tagger.load_nat(track.metadata['musicbrainz_recordingid'])
26 | for file in track.iterfiles():
27 | file.move(nat)
28 | file.metadata.delete('albumartist')
29 | file.metadata.delete('albumartistsort')
30 | file.metadata.delete('albumsort')
31 | file.metadata.delete('asin')
32 | file.metadata.delete('barcode')
33 | file.metadata.delete('catalognumber')
34 | file.metadata.delete('discnumber')
35 | file.metadata.delete('discsubtitle')
36 | file.metadata.delete('media')
37 | file.metadata.delete('musicbrainz_albumartistid')
38 | file.metadata.delete('musicbrainz_albumid')
39 | file.metadata.delete('musicbrainz_discid')
40 | file.metadata.delete('musicbrainz_releasegroupid')
41 | file.metadata.delete('releasecountry')
42 | file.metadata.delete('releasestatus')
43 | file.metadata.delete('releasetype')
44 | file.metadata.delete('totaldiscs')
45 | file.metadata.delete('totaltracks')
46 | file.metadata.delete('tracknumber')
47 | log.debug("[LoadAsNat] deleted tags: %r", file.metadata.deleted_tags)
48 |
49 |
50 | register_track_action(LoadAsNat())
51 |
--------------------------------------------------------------------------------
/plugins/moodbars/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Changelog:
4 | # [2015-09-24] Initial version with support for Ogg Vorbis, FLAC, WAV and MP3, tested MP3 and FLAC
5 |
6 | PLUGIN_NAME = u"Moodbars"
7 | PLUGIN_AUTHOR = u"Len Joubert"
8 | PLUGIN_DESCRIPTION = """Calculate Moodbars for selected files and albums."""
9 | PLUGIN_LICENSE = "GPL-2.0"
10 | PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"
11 | PLUGIN_VERSION = "0.1"
12 | PLUGIN_API_VERSIONS = ["0.10", "0.15", "0.16"]
13 | #PLUGIN_INCOMPATIBLE_PLATFORMS = [
14 | # 'win32', 'cygwyn', 'darwin', 'os2', 'os2emx', 'riscos', 'atheos']
15 |
16 | import os.path
17 | from collections import defaultdict
18 | from subprocess import check_call
19 | from picard.album import Album, NatAlbum
20 | from picard.track import Track
21 | from picard.file import File
22 | from picard.util import encode_filename, decode_filename, partial, thread
23 | from picard.ui.options import register_options_page, OptionsPage
24 | from picard.config import TextOption
25 | from picard.ui.itemviews import (BaseAction, register_file_action,
26 | register_album_action)
27 | from picard.plugins.moodbars.ui_options_moodbar import Ui_MoodbarOptionsPage
28 |
29 | # Path to various moodbar tools. There must be a tool for every supported
30 | # audio file format.
31 | MOODBAR_COMMANDS = {
32 | "Ogg Vorbis": ("moodbar_vorbis_command", "moodbar_vorbis_options"),
33 | "MPEG-1 Audio": ("moodbar_mp3_command", "moodbar_mp3_options"),
34 | "FLAC": ("moodbar_flac_command", "moodbar_flac_options"),
35 | "WavPack": ("moodbar_wav_command", "moodbar_wav_options"),
36 | }
37 |
38 |
39 | def generate_moodbar_for_files(files, format, tagger):
40 | """Generate the moodfiles for a list of files in album mode."""
41 | file_list = ['%s' % encode_filename(f.filename) for f in files]
42 | for mood_file in file_list:
43 | new_filename = os.path.join(os.path.dirname(
44 | mood_file), '.' + os.path.splitext(os.path.basename(mood_file))[0] + '.mood')
45 | # file format to make it compaitble with Amarok and hidden in linux
46 | file_list_mood = ['%s' % new_filename]
47 |
48 | if format in MOODBAR_COMMANDS \
49 | and tagger.config.setting[MOODBAR_COMMANDS[format][0]]:
50 | command = tagger.config.setting[MOODBAR_COMMANDS[format][0]]
51 | options = tagger.config.setting[
52 | MOODBAR_COMMANDS[format][1]].split(' ')
53 | # tagger.log.debug('My debug >>> %s' % (file_list_mood))
54 | tagger.log.debug(
55 | '%s %s %s %s' % (command, decode_filename(' '.join(file_list)), ' '.join(options), decode_filename(' '.join(file_list_mood))))
56 | check_call([command] + file_list + options + file_list_mood)
57 | else:
58 | raise Exception('Moodbar: Unsupported format %s' % (format))
59 |
60 |
61 | class MoodBar(BaseAction):
62 | NAME = N_("Generate Moodbar &file...")
63 |
64 | def _add_file_to_queue(self, file):
65 | thread.run_task(
66 | partial(self._generate_moodbar, file),
67 | partial(self._moodbar_callback, file))
68 |
69 | def callback(self, objs):
70 | for obj in objs:
71 | if isinstance(obj, Track):
72 | for file_ in obj.linked_files:
73 | self._add_file_to_queue(file_)
74 | elif isinstance(obj, File):
75 | self._add_file_to_queue(obj)
76 |
77 | def _generate_moodbar(self, file):
78 | self.tagger.window.set_statusbar_message(
79 | N_('Calculating moodbar for "%(filename)s"...'),
80 | {'filename': file.filename}
81 | )
82 | generate_moodbar_for_files([file], file.NAME, self.tagger)
83 |
84 | def _moodbar_callback(self, file, result=None, error=None):
85 | if not error:
86 | self.tagger.window.set_statusbar_message(
87 | N_('Moodbar for "%(filename)s" successfully generated.'),
88 | {'filename': file.filename}
89 | )
90 | else:
91 | self.tagger.window.set_statusbar_message(
92 | N_('Could not generate moodbar for "%(filename)s".'),
93 | {'filename': file.filename}
94 | )
95 |
96 |
97 | class MoodbarOptionsPage(OptionsPage):
98 |
99 | NAME = "Moodbars"
100 | TITLE = "Moodbars"
101 | PARENT = "plugins"
102 |
103 | options = [
104 | TextOption("setting", "moodbar_vorbis_command", "moodbar"),
105 | TextOption("setting", "moodbar_vorbis_options", "-o"),
106 | TextOption("setting", "moodbar_mp3_command", "moodbar"),
107 | TextOption("setting", "moodbar_mp3_options", "-o"),
108 | TextOption("setting", "moodbar_flac_command", "moodbar"),
109 | TextOption("setting", "moodbar_flac_options", "-o"),
110 | TextOption("setting", "moodbar_wav_command", "moodbar"),
111 | TextOption("setting", "moodbar_wav_options", "-o")
112 | ]
113 |
114 | def __init__(self, parent=None):
115 | super(MoodbarOptionsPage, self).__init__(parent)
116 | self.ui = Ui_MoodbarOptionsPage()
117 | self.ui.setupUi(self)
118 |
119 | def load(self):
120 | self.ui.vorbis_command.setText(
121 | self.config.setting["moodbar_vorbis_command"])
122 | self.ui.mp3_command.setText(
123 | self.config.setting["moodbar_mp3_command"])
124 | self.ui.flac_command.setText(
125 | self.config.setting["moodbar_flac_command"])
126 | self.ui.wav_command.setText(
127 | self.config.setting["moodbar_wav_command"])
128 |
129 | def save(self):
130 | self.config.setting["moodbar_vorbis_command"] = unicode(
131 | self.ui.vorbis_command.text())
132 | self.config.setting["moodbar_mp3_command"] = unicode(
133 | self.ui.mp3_command.text())
134 | self.config.setting["moodbar_flac_command"] = unicode(
135 | self.ui.flac_command.text())
136 | self.config.setting["moodbar_wav_command"] = unicode(
137 | self.ui.wav_command.text())
138 |
139 | register_file_action(MoodBar())
140 | register_options_page(MoodbarOptionsPage)
141 |
--------------------------------------------------------------------------------
/plugins/moodbars/options_moodbar.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | MoodbarOptionsPage
4 |
5 |
6 |
7 | 0
8 | 0
9 | 305
10 | 317
11 |
12 |
13 |
14 |
15 | 6
16 |
17 |
18 | 9
19 |
20 | -
21 |
22 |
23 | Moodbar
24 |
25 |
26 |
27 | 2
28 |
29 |
30 | 9
31 |
32 | -
33 |
34 |
35 | Path to Ogg Vorbis moodbar tool:
36 |
37 |
38 |
39 | -
40 |
41 |
42 | -
43 |
44 |
45 | Path to MP3 moodbar tool:
46 |
47 |
48 |
49 | -
50 |
51 |
52 | -
53 |
54 |
55 | Path to FLAC moodbar tool:
56 |
57 |
58 |
59 | -
60 |
61 |
62 | -
63 |
64 |
65 | Path to WAV moodbar tool:
66 |
67 |
68 |
69 | -
70 |
71 |
72 |
73 |
74 |
75 | -
76 |
77 |
78 | Qt::Vertical
79 |
80 |
81 |
82 | 263
83 | 21
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/plugins/moodbars/ui_options_moodbar.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Form implementation generated from reading ui file 'options_moodbar.ui'
4 | #
5 | # Created: Thu Sep 24 14:14:45 2015
6 | # by: PyQt4 UI code generator 4.11.3
7 | #
8 | # WARNING! All changes made in this file will be lost!
9 |
10 | from PyQt4 import QtCore, QtGui
11 |
12 | try:
13 | _fromUtf8 = QtCore.QString.fromUtf8
14 | except AttributeError:
15 | def _fromUtf8(s):
16 | return s
17 |
18 | try:
19 | _encoding = QtGui.QApplication.UnicodeUTF8
20 | def _translate(context, text, disambig):
21 | return QtGui.QApplication.translate(context, text, disambig, _encoding)
22 | except AttributeError:
23 | def _translate(context, text, disambig):
24 | return QtGui.QApplication.translate(context, text, disambig)
25 |
26 | class Ui_MoodbarOptionsPage(object):
27 | def setupUi(self, MoodbarOptionsPage):
28 | MoodbarOptionsPage.setObjectName(_fromUtf8("MoodbarOptionsPage"))
29 | MoodbarOptionsPage.resize(305, 317)
30 | self.vboxlayout = QtGui.QVBoxLayout(MoodbarOptionsPage)
31 | self.vboxlayout.setSpacing(6)
32 | self.vboxlayout.setMargin(9)
33 | self.vboxlayout.setObjectName(_fromUtf8("vboxlayout"))
34 | self.moodbar_group = QtGui.QGroupBox(MoodbarOptionsPage)
35 | self.moodbar_group.setObjectName(_fromUtf8("moodbar_group"))
36 | self.vboxlayout1 = QtGui.QVBoxLayout(self.moodbar_group)
37 | self.vboxlayout1.setSpacing(2)
38 | self.vboxlayout1.setMargin(9)
39 | self.vboxlayout1.setObjectName(_fromUtf8("vboxlayout1"))
40 | self.label = QtGui.QLabel(self.moodbar_group)
41 | self.label.setObjectName(_fromUtf8("label"))
42 | self.vboxlayout1.addWidget(self.label)
43 | self.vorbis_command = QtGui.QLineEdit(self.moodbar_group)
44 | self.vorbis_command.setObjectName(_fromUtf8("vorbis_command"))
45 | self.vboxlayout1.addWidget(self.vorbis_command)
46 | self.label_2 = QtGui.QLabel(self.moodbar_group)
47 | self.label_2.setObjectName(_fromUtf8("label_2"))
48 | self.vboxlayout1.addWidget(self.label_2)
49 | self.mp3_command = QtGui.QLineEdit(self.moodbar_group)
50 | self.mp3_command.setObjectName(_fromUtf8("mp3_command"))
51 | self.vboxlayout1.addWidget(self.mp3_command)
52 | self.label_3 = QtGui.QLabel(self.moodbar_group)
53 | self.label_3.setObjectName(_fromUtf8("label_3"))
54 | self.vboxlayout1.addWidget(self.label_3)
55 | self.flac_command = QtGui.QLineEdit(self.moodbar_group)
56 | self.flac_command.setObjectName(_fromUtf8("flac_command"))
57 | self.vboxlayout1.addWidget(self.flac_command)
58 | self.label_4 = QtGui.QLabel(self.moodbar_group)
59 | self.label_4.setObjectName(_fromUtf8("label_4"))
60 | self.vboxlayout1.addWidget(self.label_4)
61 | self.wav_command = QtGui.QLineEdit(self.moodbar_group)
62 | self.wav_command.setObjectName(_fromUtf8("wav_command"))
63 | self.vboxlayout1.addWidget(self.wav_command)
64 | self.vboxlayout.addWidget(self.moodbar_group)
65 | spacerItem = QtGui.QSpacerItem(263, 21, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
66 | self.vboxlayout.addItem(spacerItem)
67 |
68 | self.retranslateUi(MoodbarOptionsPage)
69 | QtCore.QMetaObject.connectSlotsByName(MoodbarOptionsPage)
70 |
71 | def retranslateUi(self, MoodbarOptionsPage):
72 | self.moodbar_group.setTitle(_translate("MoodbarOptionsPage", "Moodbar", None))
73 | self.label.setText(_translate("MoodbarOptionsPage", "Path to Ogg Vorbis moodbar tool:", None))
74 | self.label_2.setText(_translate("MoodbarOptionsPage", "Path to MP3 moodbar tool:", None))
75 | self.label_3.setText(_translate("MoodbarOptionsPage", "Path to FLAC moodbar tool:", None))
76 | self.label_4.setText(_translate("MoodbarOptionsPage", "Path to WAV moodbar tool:", None))
77 |
78 |
--------------------------------------------------------------------------------
/plugins/musixmatch/README:
--------------------------------------------------------------------------------
1 | Installation:
2 | Obtain API key from Musixmatch (https://developer.musixmatch.com)
3 |
--------------------------------------------------------------------------------
/plugins/musixmatch/__init__.py:
--------------------------------------------------------------------------------
1 | PLUGIN_NAME = 'Musixmatch Lyrics'
2 | PLUGIN_AUTHOR = 'm-yn'
3 | PLUGIN_DESCRIPTION = 'Fetch first 30% of lyrics from Musixmatch'
4 | PLUGIN_VERSION = '0.2'
5 | PLUGIN_API_VERSIONS = ["0.9.0", "0.10", "0.15"]
6 | PLUGIN_LICENSE = "GPL-2.0"
7 | PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"
8 |
9 |
10 | from picard.metadata import register_track_metadata_processor
11 | from picard.ui.options import register_options_page, OptionsPage
12 | from picard.config import TextOption
13 | from ui_options_musixmatch import Ui_MusixmatchOptionsPage
14 |
15 |
16 | class MusixmatchOptionsPage(OptionsPage):
17 | NAME = 'musixmatch'
18 | TITLE = 'Musixmatch API Key'
19 | PARENT = "plugins"
20 | options = [
21 | TextOption("setting", "musixmatch_api_key", "")
22 | ]
23 |
24 | def __init__(self, parent=None):
25 | super(MusixmatchOptionsPage, self).__init__(parent)
26 | self.ui = Ui_MusixmatchOptionsPage()
27 | self.ui.setupUi(self)
28 |
29 | def load(self):
30 | self.ui.api_key.setText(self.config.setting["musixmatch_api_key"])
31 |
32 | def save(self):
33 | self.config.setting["musixmatch_api_key"] = self.ui.api_key.text()
34 | register_options_page(MusixmatchOptionsPage)
35 |
36 | import picard.tagger as tagger
37 | import os
38 | try:
39 | os.environ['MUSIXMATCH_API_KEY'] = tagger.config.setting[
40 | "musixmatch_api_key"]
41 | except:
42 | pass
43 |
44 | from musixmatch import track as TRACK
45 |
46 |
47 | def process_track(album, metadata, release, track):
48 | if('MUSIXMATCH_API_KEY' not in os.environ):
49 | return
50 | try:
51 | t = TRACK.Track(metadata['musicbrainz_trackid'],
52 | musicbrainz=True).lyrics()
53 | if t['instrumental'] == 1:
54 | lyrics = "[Instrumental]"
55 | else:
56 | lyrics = t['lyrics_body']
57 | metadata['lyrics:description'] = lyrics
58 | except Exception as e:
59 | pass
60 |
61 | register_track_metadata_processor(process_track)
62 |
--------------------------------------------------------------------------------
/plugins/musixmatch/musixmatch/__init__.py:
--------------------------------------------------------------------------------
1 | PLUGIN_NAME = 'Musixmatch Lyrics'
2 | PLUGIN_AUTHOR = 'm-yn'
3 | PLUGIN_DESCRIPTION = 'Fetch first 30% of lyrics from Musixmatch'
4 | PLUGIN_VERSION = '0.2'
5 | PLUGIN_API_VERSIONS = ["0.9.0", "0.10", "0.15"]
6 | PLUGIN_LICENSE = "GPL-2.0"
7 | PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"
8 |
--------------------------------------------------------------------------------
/plugins/musixmatch/ui_options_musixmatch.py:
--------------------------------------------------------------------------------
1 | from PyQt4 import QtCore, QtGui
2 |
3 | try:
4 | _fromUtf8 = QtCore.QString.fromUtf8
5 | except AttributeError:
6 | _fromUtf8 = lambda s: s
7 |
8 | class Ui_MusixmatchOptionsPage(object):
9 | def setupUi(self, MusixmatchOptionsPage):
10 | MusixmatchOptionsPage.setObjectName(_fromUtf8("MusixmatchOptionsPage"))
11 | MusixmatchOptionsPage.resize(305, 317)
12 | self.vboxlayout = QtGui.QVBoxLayout(MusixmatchOptionsPage)
13 | self.vboxlayout.setMargin(9)
14 | self.vboxlayout.setSpacing(6)
15 | self.vboxlayout.setObjectName(_fromUtf8("vboxlayout"))
16 | self.api_key_group = QtGui.QGroupBox(MusixmatchOptionsPage)
17 | self.api_key_group.setObjectName(_fromUtf8("api_key_group"))
18 | self.vboxlayout2 = QtGui.QVBoxLayout(self.api_key_group)
19 | self.vboxlayout2.setMargin(9)
20 | self.vboxlayout2.setSpacing(2)
21 | self.vboxlayout2.setObjectName(_fromUtf8("vboxlayout2"))
22 | self.api_key_label = QtGui.QLabel(self.api_key_group)
23 | self.api_key_label.setObjectName(_fromUtf8("api_key_label"))
24 | self.vboxlayout2.addWidget(self.api_key_label)
25 | self.api_key = QtGui.QLineEdit(self.api_key_group)
26 | self.api_key.setObjectName(_fromUtf8("api_key"))
27 | self.vboxlayout2.addWidget(self.api_key)
28 | self.vboxlayout.addWidget(self.api_key_group)
29 | spacerItem = QtGui.QSpacerItem(263, 21, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
30 | self.vboxlayout.addItem(spacerItem)
31 |
32 | self.retranslateUi(MusixmatchOptionsPage)
33 | QtCore.QMetaObject.connectSlotsByName(MusixmatchOptionsPage)
34 |
35 |
36 |
37 |
38 | def retranslateUi(self, MusixmatchOptionsPage):
39 | self.api_key_label.setText(_("Musixmatch API Key"))
40 |
--------------------------------------------------------------------------------
/plugins/no_release/no_release.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | PLUGIN_NAME = u'No release'
4 | PLUGIN_AUTHOR = u'Johannes Weißl'
5 | PLUGIN_DESCRIPTION = '''Do not store specific release information in releases of unknown origin.'''
6 | PLUGIN_VERSION = '0.1'
7 | PLUGIN_API_VERSIONS = ['0.15']
8 |
9 | from PyQt4 import QtCore, QtGui
10 |
11 | from picard.album import Album
12 | from picard.metadata import register_album_metadata_processor, register_track_metadata_processor
13 | from picard.ui.options import register_options_page, OptionsPage
14 | from picard.ui.itemviews import BaseAction, register_album_action
15 | from picard.config import BoolOption, TextOption
16 |
17 |
18 | class Ui_NoReleaseOptionsPage(object):
19 |
20 | def setupUi(self, NoReleaseOptionsPage):
21 | NoReleaseOptionsPage.setObjectName('NoReleaseOptionsPage')
22 | NoReleaseOptionsPage.resize(394, 300)
23 | self.verticalLayout = QtGui.QVBoxLayout(NoReleaseOptionsPage)
24 | self.verticalLayout.setObjectName('verticalLayout')
25 | self.groupBox = QtGui.QGroupBox(NoReleaseOptionsPage)
26 | self.groupBox.setObjectName('groupBox')
27 | self.vboxlayout = QtGui.QVBoxLayout(self.groupBox)
28 | self.vboxlayout.setObjectName('vboxlayout')
29 | self.norelease_enable = QtGui.QCheckBox(self.groupBox)
30 | self.norelease_enable.setObjectName('norelease_enable')
31 | self.vboxlayout.addWidget(self.norelease_enable)
32 | self.label = QtGui.QLabel(self.groupBox)
33 | self.label.setObjectName('label')
34 | self.vboxlayout.addWidget(self.label)
35 | self.horizontalLayout = QtGui.QHBoxLayout()
36 | self.horizontalLayout.setObjectName('horizontalLayout')
37 | self.norelease_strip_tags = QtGui.QLineEdit(self.groupBox)
38 | self.norelease_strip_tags.setObjectName('norelease_strip_tags')
39 | self.horizontalLayout.addWidget(self.norelease_strip_tags)
40 | self.vboxlayout.addLayout(self.horizontalLayout)
41 | self.verticalLayout.addWidget(self.groupBox)
42 | spacerItem = QtGui.QSpacerItem(368, 187, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
43 | self.verticalLayout.addItem(spacerItem)
44 |
45 | self.retranslateUi(NoReleaseOptionsPage)
46 | QtCore.QMetaObject.connectSlotsByName(NoReleaseOptionsPage)
47 |
48 | def retranslateUi(self, NoReleaseOptionsPage):
49 | self.groupBox.setTitle(QtGui.QApplication.translate('NoReleaseOptionsPage', 'No release', None, QtGui.QApplication.UnicodeUTF8))
50 | self.norelease_enable.setText(QtGui.QApplication.translate('NoReleaseOptionsPage', _('Enable plugin for all releases by default'), None, QtGui.QApplication.UnicodeUTF8))
51 | self.label.setText(QtGui.QApplication.translate('NoReleaseOptionsPage', _('Tags to strip (comma-separated)'), None, QtGui.QApplication.UnicodeUTF8))
52 |
53 |
54 | def strip_release_specific_metadata(tagger, metadata):
55 | strip_tags = tagger.config.setting['norelease_strip_tags']
56 | strip_tags = [tag.strip() for tag in strip_tags.split(',')]
57 | for tag in strip_tags:
58 | if tag in metadata:
59 | del metadata[tag]
60 |
61 |
62 | class NoReleaseAction(BaseAction):
63 | NAME = _('Remove specific release information...')
64 |
65 | def callback(self, objs):
66 | for album in objs:
67 | if isinstance(album, Album):
68 | strip_release_specific_metadata(self.tagger, album.metadata)
69 | for track in album.tracks:
70 | strip_release_specific_metadata(self.tagger, track.metadata)
71 | for file in track.linked_files:
72 | track.update_file_metadata(file)
73 | album.update()
74 |
75 |
76 | class NoReleaseOptionsPage(OptionsPage):
77 | NAME = 'norelease'
78 | TITLE = 'No release'
79 | PARENT = 'plugins'
80 |
81 | options = [
82 | BoolOption('setting', 'norelease_enable', False),
83 | TextOption('setting', 'norelease_strip_tags', 'asin,barcode,catalognumber,date,label,media,releasecountry,releasestatus'),
84 | ]
85 |
86 | def __init__(self, parent=None):
87 | super(NoReleaseOptionsPage, self).__init__(parent)
88 | self.ui = Ui_NoReleaseOptionsPage()
89 | self.ui.setupUi(self)
90 |
91 | def load(self):
92 | self.ui.norelease_strip_tags.setText(self.config.setting['norelease_strip_tags'])
93 | self.ui.norelease_enable.setChecked(self.config.setting['norelease_enable'])
94 |
95 | def save(self):
96 | self.config.setting['norelease_strip_tags'] = unicode(self.ui.norelease_strip_tags.text())
97 | self.config.setting['norelease_enable'] = self.ui.norelease_enable.isChecked()
98 |
99 |
100 | def NoReleaseAlbumProcessor(tagger, metadata, release):
101 | if tagger.config.setting['norelease_enable']:
102 | strip_release_specific_metadata(tagger, metadata)
103 |
104 |
105 | def NoReleaseTrackProcessor(tagger, metadata, track, release):
106 | if tagger.config.setting['norelease_enable']:
107 | strip_release_specific_metadata(tagger, metadata)
108 |
109 | register_album_metadata_processor(NoReleaseAlbumProcessor)
110 | register_track_metadata_processor(NoReleaseTrackProcessor)
111 | register_album_action(NoReleaseAction())
112 | register_options_page(NoReleaseOptionsPage)
113 |
--------------------------------------------------------------------------------
/plugins/non_ascii_equivalents/non_ascii_equivalents.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Copyright (C) 2016 Anderson Mesquita
4 | #
5 | # This program is free software: you can redistribute it and/or modify it under
6 | # the terms of the GNU General Public License as published by the Free Software
7 | # Foundation, either version 3 of the License, or (at your option) any later
8 | # version.
9 | #
10 | # This program is distributed in the hope that it will be useful, but WITHOUT
11 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
13 | # details.
14 | #
15 | # You should have received a copy of the GNU General Public License along with
16 | # this program. If not, see .
17 |
18 | from picard import metadata
19 |
20 | PLUGIN_NAME = "Non-ASCII Equivalents"
21 | PLUGIN_AUTHOR = "Anderson Mesquita "
22 | PLUGIN_VERSION = "0.1"
23 | PLUGIN_API_VERSIONS = ["0.9", "0.10", "0.11", "0.15"]
24 | PLUGIN_LICENSE = "GPLv3"
25 | PLUGIN_LICENSE_URL = "https://gnu.org/licenses/gpl.html"
26 | PLUGIN_DESCRIPTION = '''Replaces accented and otherwise non-ASCII characters
27 | with a somewhat equivalent version of their ASCII counterparts. This allows old
28 | devices to be able to display song artists and titles somewhat correctly,
29 | instead of displaying weird or blank symbols. It's an attempt to do a little
30 | better than Musicbrainz's native "Replace non-ASCII characters" option.'''
31 |
32 | CHAR_TABLE = {
33 | # Acute # Grave # Umlaut # Circumflex
34 | u"Á": "A", u"À": "A", u"Ä": "A", u"Â": "A",
35 | u"É": "E", u"È": "E", u"Ë": "E", u"Ê": "E",
36 | u"Í": "I", u"Ì": "I", u"Ï": "I", u"Î": "I",
37 | u"Ó": "O", u"Ò": "O", u"Ö": "O", u"Ô": "O",
38 | u"Ú": "U", u"Ù": "U", u"Ü": "U", u"Û": "U",
39 | u"Ý": "Y", u"Ỳ": "Y", u"Ÿ": "Y", u"Ŷ": "Y",
40 | u"á": "a", u"à": "a", u"ä": "a", u"â": "a",
41 | u"é": "e", u"è": "e", u"ë": "e", u"ê": "e",
42 | u"í": "i", u"ì": "i", u"ï": "i", u"î": "i",
43 | u"ó": "o", u"ò": "o", u"ö": "o", u"ô": "o",
44 | u"ú": "u", u"ù": "u", u"ü": "u", u"û": "u",
45 | u"ý": "y", u"ỳ": "y", u"ÿ": "y", u"ŷ": "y",
46 |
47 | # Misc Letters
48 | u"Å": "AA",
49 | u"å": "aa",
50 | u"Æ": "AE",
51 | u"æ": "ae",
52 | u"Œ": "OE",
53 | u"œ": "oe",
54 | u"ẞ": "ss",
55 | u"ß": "ss",
56 | u"Ç": "C",
57 | u"ç": "c",
58 | u"Ñ": "N",
59 | u"ñ": "n",
60 | u"Ø": "O",
61 | u"ø": "o",
62 |
63 | # Punctuation
64 | u"¡": "!",
65 | u"¿": "?",
66 | u"–": "--",
67 | u"—": "--",
68 | u"―": "--",
69 | u"«": "<<",
70 | u"»": ">>",
71 | u"‘": "'",
72 | u"’": "'",
73 | u"‚": ",",
74 | u"‛": "'",
75 | u"“": '"',
76 | u"”": '"',
77 | u"„": ",,",
78 | u"‟": '"',
79 | u"‹": "<",
80 | u"›": ">",
81 | u"⹂": ",,",
82 | u"「": "|-",
83 | u"」": "-|",
84 | u"『": "|-",
85 | u"』": "-|",
86 | u"〝": '"',
87 | u"〞": '"',
88 | u"〟": ",,",
89 | u"﹁": "-|",
90 | u"﹂": "|-",
91 | u"﹃": "-|",
92 | u"﹄": "|-",
93 | u""": '"',
94 | u"'": "'",
95 | u"「": "|-",
96 | u"」": "-|",
97 |
98 | # Mathematics
99 | u"≠": "!=",
100 | u"≤": "<=",
101 | u"≥": ">=",
102 | u"±": "+-",
103 | u"∓": "-+",
104 | u"×": "x",
105 | u"·": ".",
106 | u"÷": "/",
107 | u"√": "\/",
108 | u"∑": "E",
109 | u"≪": "<<", # these are different
110 | u"≫": ">>", # from the quotation marks
111 |
112 | # Misc
113 | u"ª": "a",
114 | u"º": "o",
115 | u"°": "o",
116 | u"µ": "u",
117 | u"ı": "i",
118 | u"†": "t",
119 | u"©": "(c)",
120 | u"®": "(R)",
121 | u"℠": "(SM)",
122 | u"™": "(TM)",
123 | }
124 |
125 | FILTER_TAGS = [
126 | "album",
127 | "artist",
128 | "title",
129 | ]
130 |
131 |
132 | def sanitize(char):
133 | if char in CHAR_TABLE:
134 | return CHAR_TABLE[char]
135 | return char
136 |
137 |
138 | def ascii(word):
139 | return "".join(sanitize(char) for char in word)
140 |
141 |
142 | def main(tagger, metadata, release, track=None):
143 | for name, value in metadata.rawitems():
144 | if name in FILTER_TAGS:
145 | metadata[name] = [ascii(x) for x in value]
146 |
147 |
148 | metadata.register_track_metadata_processor(main)
149 | metadata.register_album_metadata_processor(main)
150 |
--------------------------------------------------------------------------------
/plugins/padded/padded.py:
--------------------------------------------------------------------------------
1 | PLUGIN_NAME = "Padded disc and tracknumbers"
2 | PLUGIN_AUTHOR = "Wieland Hoffmann"
3 | PLUGIN_DESCRIPTION = """
4 | Adds padded disc- and tracknumbers so the length of all disc- and tracknumbers
5 | is the same. They are stored in the `_paddedtracknumber` and `_paddeddiscnumber`
6 | tags."""
7 |
8 | PLUGIN_VERSION = "1.0"
9 | PLUGIN_API_VERSIONS = ["0.15.0", "0.15.1", "0.16.0", "1.0.0", "1.1.0", "1.2.0",
10 | "1.3.0", "2.0", ]
11 | PLUGIN_LICENSE = "GPL-2.0 or later"
12 | PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"
13 |
14 | from picard.metadata import register_track_metadata_processor
15 |
16 |
17 | @register_track_metadata_processor
18 | def add_padded_tn(album, metadata, release, track):
19 | maxlength = len(metadata["totaltracks"])
20 | islength = len(metadata["tracknumber"])
21 | metadata["~paddedtracknumber"] = (int(maxlength - islength) * "0" +
22 | metadata["tracknumber"])
23 |
24 |
25 | @register_track_metadata_processor
26 | def add_padded_dn(album, metadata, release, track):
27 | maxlength = len(metadata["totaldiscs"])
28 | islength = len(metadata["discnumber"])
29 | metadata["~paddeddiscnumber"] = (int(maxlength - islength) * "0" +
30 | metadata["discnumber"])
31 |
--------------------------------------------------------------------------------
/plugins/papercdcase/papercdcase.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2015 Philipp Wolfer
4 | #
5 | # This program is free software; you can redistribute it and/or
6 | # modify it under the terms of the GNU General Public License
7 | # as published by the Free Software Foundation; either version 2
8 | # of the License, or (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program; if not, write to the Free Software
17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 | # 02110-1301, USA.
19 |
20 | PLUGIN_NAME = u'Paper CD case'
21 | PLUGIN_AUTHOR = u'Philipp Wolfer'
22 | PLUGIN_DESCRIPTION = u'Create a paper CD case from an album or cluster using http://papercdcase.com'
23 | PLUGIN_VERSION = "0.1"
24 | PLUGIN_API_VERSIONS = ["1.3.0", "1.4.0"]
25 | PLUGIN_LICENSE = "GPL-2.0"
26 | PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"
27 |
28 |
29 | from PyQt4.QtCore import QUrl
30 | from picard.album import Album
31 | from picard.cluster import Cluster
32 | from picard.ui.itemviews import BaseAction, register_album_action, register_cluster_action
33 | from picard.util import webbrowser2
34 | from picard.util import textencoding
35 |
36 |
37 | PAPERCDCASE_URL = 'http://papercdcase.com/advanced.php'
38 |
39 |
40 | def build_papercdcase_url(artist, album, tracks):
41 | url = QUrl(PAPERCDCASE_URL)
42 | # papercdcase.com does not deal well with unicode characters :(
43 | url.addQueryItem('artist', textencoding.asciipunct(artist))
44 | url.addQueryItem('title', textencoding.asciipunct(album))
45 | i = 1
46 | for track in tracks:
47 | url.addQueryItem('track' + str(i), textencoding.asciipunct(track))
48 | i += 1
49 | return url.toString()
50 |
51 |
52 | class PaperCdCase(BaseAction):
53 | NAME = 'Create paper CD case'
54 |
55 | def callback(self, objs):
56 | for obj in objs:
57 | if isinstance(obj, Album) or isinstance(obj, Cluster):
58 | artist = obj.metadata['albumartist']
59 | album = obj.metadata['album']
60 | if isinstance(obj, Album):
61 | tracks = [track.metadata['title'] for track in obj.tracks]
62 | else:
63 | tracks = [file.metadata['title'] for file in obj.files]
64 | url = build_papercdcase_url(artist, album, tracks)
65 | webbrowser2.open(url)
66 |
67 |
68 | paperCdCase = PaperCdCase()
69 | register_album_action(paperCdCase)
70 | register_cluster_action(paperCdCase)
71 |
--------------------------------------------------------------------------------
/plugins/playlist/playlist.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*-
3 |
4 | # This program is free software; you can redistribute it and/or
5 | # modify it under the terms of the GNU General Public License
6 | # as published by the Free Software Foundation; either version 2
7 | # of the License, or (at your option) any later version.
8 | #
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 |
14 | PLUGIN_NAME = u"Generate M3U playlist"
15 | PLUGIN_AUTHOR = u"Francis Chin"
16 | PLUGIN_DESCRIPTION = u"""Generate an Extended M3U playlist (.m3u8 file, UTF8
17 | encoded text). Relative pathnames are used where audio files are in the same
18 | directory as the playlist, otherwise absolute (full) pathnames are used."""
19 | PLUGIN_VERSION = "0.3"
20 | PLUGIN_API_VERSIONS = ["1.0.0", "1.1.0", "1.2.0", "1.3.0"]
21 | PLUGIN_LICENSE = "GPL-2.0"
22 | PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"
23 |
24 | import os.path
25 |
26 | from PyQt4 import QtCore, QtGui
27 | from picard import log
28 | from picard.const import VARIOUS_ARTISTS_ID
29 | from picard.util import find_existing_path, encode_filename
30 | from picard.ui.itemviews import BaseAction, register_album_action
31 |
32 |
33 | _debug_level = 0
34 |
35 | def get_safe_filename(filename):
36 | _valid_chars = u" .,_-:+&!()"
37 | _safe_filename = u"".join(
38 | c if (c.isalnum() or c in _valid_chars) else "_" for c in filename
39 | ).rstrip()
40 | return _safe_filename
41 |
42 |
43 | class PlaylistEntry(list):
44 |
45 | def __init__(self, playlist, index):
46 | list.__init__(self)
47 | self.playlist = playlist
48 | self.index = index
49 |
50 | def add(self, entry_row):
51 | self.append(entry_row + "\n")
52 |
53 |
54 | class Playlist(object):
55 |
56 | def __init__(self, filename):
57 | self.filename = filename
58 | self.entries = []
59 | self.headers = []
60 |
61 | def add_header(self, header):
62 | self.headers.append(header + "\n")
63 |
64 | def write(self):
65 | b_lines = []
66 | for header in self.headers:
67 | b_lines.append(header.encode("utf-8"))
68 | for entry in self.entries:
69 | for row in entry:
70 | b_lines.append(unicode(row).encode("utf-8"))
71 | with open(encode_filename(self.filename), "wt") as f:
72 | f.writelines(b_lines)
73 |
74 |
75 | class GeneratePlaylist(BaseAction):
76 | NAME = "Generate &Playlist..."
77 |
78 | def callback(self, objs):
79 | current_directory = (self.config.persist["current_directory"]
80 | or QtCore.QDir.homePath())
81 | current_directory = find_existing_path(unicode(current_directory))
82 | # Default playlist filename set as "%albumartist% - %album%.m3u8",
83 | # except where "Various Artists" is suppressed
84 | if _debug_level > 1:
85 | log.debug("{}: VARIOUS_ARTISTS_ID is {}, musicbrainz_albumartistid is {}".format(
86 | PLUGIN_NAME, VARIOUS_ARTISTS_ID,
87 | objs[0].metadata["musicbrainz_albumartistid"]))
88 | if objs[0].metadata["musicbrainz_albumartistid"] != VARIOUS_ARTISTS_ID:
89 | default_filename = get_safe_filename(
90 | objs[0].metadata["albumartist"] + " - "
91 | + objs[0].metadata["album"] + ".m3u8"
92 | )
93 | else:
94 | default_filename = get_safe_filename(
95 | objs[0].metadata["album"] + ".m3u8"
96 | )
97 | if _debug_level > 1:
98 | log.debug("{}: default playlist filename sanitized to {}".format(
99 | PLUGIN_NAME, default_filename))
100 | b_filename, b_selected_format = QtGui.QFileDialog.getSaveFileNameAndFilter(
101 | None, "Save new playlist",
102 | os.path.join(current_directory, default_filename),
103 | "Playlist (*.m3u8 *.m3u)"
104 | )
105 | if b_filename:
106 | filename = unicode(b_filename)
107 | playlist = Playlist(filename)
108 | playlist.add_header(u"#EXTM3U")
109 |
110 | for album in objs:
111 | for track in album.tracks:
112 | if track.linked_files:
113 | entry = PlaylistEntry(playlist, len(playlist.entries))
114 | playlist.entries.append(entry)
115 |
116 | # M3U EXTINF row
117 | track_length_seconds = int(round(track.metadata.length / 1000.0))
118 | # EXTINF format assumed to be fixed as follows:
119 | entry.add(u"#EXTINF:{duration:d},{artist} - {title}".format(
120 | duration=track_length_seconds,
121 | artist=track.metadata["artist"],
122 | title=track.metadata["title"]
123 | )
124 | )
125 |
126 | # M3U URL row - assumes only one file per track
127 | audio_filename = track.linked_files[0].filename
128 | if _debug_level > 1:
129 | for i, file in enumerate(track.linked_files):
130 | log.debug("{}: linked_file {}: {}".format(
131 | PLUGIN_NAME, i, str(file)))
132 | # If playlist is in same directory as audio files, then use
133 | # local (relative) pathname, otherwise use absolute pathname
134 | if _debug_level > 1:
135 | log.debug("{}: audio_filename: {}, selected dir: {}".format(
136 | PLUGIN_NAME, audio_filename, os.path.dirname(filename)))
137 | if os.path.dirname(filename) == os.path.dirname(audio_filename):
138 | audio_filename = os.path.basename(audio_filename)
139 | entry.add(unicode(audio_filename))
140 |
141 | playlist.write()
142 |
143 |
144 | register_album_action(GeneratePlaylist())
145 |
--------------------------------------------------------------------------------
/plugins/release_type/release_type.py:
--------------------------------------------------------------------------------
1 | PLUGIN_NAME = 'Release Type'
2 | PLUGIN_AUTHOR = 'Elliot Chance'
3 | PLUGIN_DESCRIPTION = 'Appends information to EPs and Singles'
4 | PLUGIN_VERSION = '1.3'
5 | PLUGIN_API_VERSIONS = ["0.9.0", "0.10", "0.15"]
6 |
7 | from picard.metadata import register_album_metadata_processor
8 |
9 | RELEASE_TYPE_MAPPING = {
10 | "ep": " EP",
11 | "single": " (single)"
12 | }
13 |
14 |
15 | def add_release_type(tagger, metadata, release):
16 |
17 | # make sure "EP" (or "single", ...) is not already a word in the name
18 | words = metadata["album"].lower().split(" ")
19 | for word in ["ep", "e.p.", "single", "(single)"]:
20 | if word in words:
21 | return
22 |
23 | # check release type
24 | for releasetype, text in RELEASE_TYPE_MAPPING.iteritems():
25 | if (metadata["~primaryreleasetype"] == releasetype) or (
26 | metadata["releasetype"].startswith(releasetype)):
27 | rs = text
28 | break
29 | else:
30 | rs = ""
31 |
32 | # append title
33 | metadata["album"] = metadata["album"] + rs
34 |
35 | register_album_metadata_processor(add_release_type)
36 |
--------------------------------------------------------------------------------
/plugins/remove_perfect_albums/remove_perfect_albums.py:
--------------------------------------------------------------------------------
1 | PLUGIN_NAME = u'Remove Perfect Albums'
2 | PLUGIN_AUTHOR = u'ichneumon, hrglgrmpf'
3 | PLUGIN_DESCRIPTION = u'''Remove all perfectly matched albums from the selection.'''
4 | PLUGIN_VERSION = '0.2'
5 | PLUGIN_API_VERSIONS = ['0.15']
6 | PLUGIN_LICENSE = "GPL-2.0"
7 | PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"
8 |
9 | from picard.album import Album
10 | from picard.ui.itemviews import BaseAction, register_album_action
11 |
12 | class RemovePerfectAlbums(BaseAction):
13 | NAME = 'Remove perfect albums'
14 |
15 | def callback(self, objs):
16 | for album in objs:
17 | if isinstance(album, Album) and album.is_complete() and album.get_num_unmatched_files() == 0\
18 | and album.get_num_matched_tracks() == len(list(album.iterfiles()))\
19 | and album.get_num_unsaved_files() == 0 and album.loaded == True:
20 | self.tagger.remove_album(album)
21 |
22 | register_album_action(RemovePerfectAlbums())
23 |
--------------------------------------------------------------------------------
/plugins/replaygain/options_replaygain.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | ReplayGainOptionsPage
4 |
5 |
6 |
7 | 0
8 | 0
9 | 305
10 | 317
11 |
12 |
13 |
14 |
15 | 6
16 |
17 |
18 | 9
19 |
20 | -
21 |
22 |
23 | Replay Gain
24 |
25 |
26 |
27 | 2
28 |
29 |
30 | 9
31 |
32 | -
33 |
34 |
35 | Path to VorbisGain:
36 |
37 |
38 |
39 | -
40 |
41 |
42 | -
43 |
44 |
45 | Path to MP3Gain:
46 |
47 |
48 |
49 | -
50 |
51 |
52 | -
53 |
54 |
55 | Path to metaflac:
56 |
57 |
58 |
59 | -
60 |
61 |
62 | -
63 |
64 |
65 | Path to wvgain:
66 |
67 |
68 |
69 | -
70 |
71 |
72 |
73 |
74 |
75 | -
76 |
77 |
78 | Qt::Vertical
79 |
80 |
81 |
82 | 263
83 | 21
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/plugins/replaygain/ui_options_replaygain.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Automatically generated - don't edit.
4 | # Use `python setup.py build_ui` to update it.
5 |
6 | from PyQt4 import QtCore, QtGui
7 |
8 | try:
9 | _fromUtf8 = QtCore.QString.fromUtf8
10 | except AttributeError:
11 | _fromUtf8 = lambda s: s
12 |
13 | class Ui_ReplayGainOptionsPage(object):
14 | def setupUi(self, ReplayGainOptionsPage):
15 | ReplayGainOptionsPage.setObjectName(_fromUtf8("ReplayGainOptionsPage"))
16 | ReplayGainOptionsPage.resize(305, 317)
17 | self.vboxlayout = QtGui.QVBoxLayout(ReplayGainOptionsPage)
18 | self.vboxlayout.setSpacing(6)
19 | self.vboxlayout.setMargin(9)
20 | self.vboxlayout.setObjectName(_fromUtf8("vboxlayout"))
21 | self.replay_gain = QtGui.QGroupBox(ReplayGainOptionsPage)
22 | self.replay_gain.setObjectName(_fromUtf8("replay_gain"))
23 | self.vboxlayout1 = QtGui.QVBoxLayout(self.replay_gain)
24 | self.vboxlayout1.setSpacing(2)
25 | self.vboxlayout1.setMargin(9)
26 | self.vboxlayout1.setObjectName(_fromUtf8("vboxlayout1"))
27 | self.label = QtGui.QLabel(self.replay_gain)
28 | self.label.setObjectName(_fromUtf8("label"))
29 | self.vboxlayout1.addWidget(self.label)
30 | self.vorbisgain_command = QtGui.QLineEdit(self.replay_gain)
31 | self.vorbisgain_command.setObjectName(_fromUtf8("vorbisgain_command"))
32 | self.vboxlayout1.addWidget(self.vorbisgain_command)
33 | self.label_2 = QtGui.QLabel(self.replay_gain)
34 | self.label_2.setObjectName(_fromUtf8("label_2"))
35 | self.vboxlayout1.addWidget(self.label_2)
36 | self.mp3gain_command = QtGui.QLineEdit(self.replay_gain)
37 | self.mp3gain_command.setObjectName(_fromUtf8("mp3gain_command"))
38 | self.vboxlayout1.addWidget(self.mp3gain_command)
39 | self.label_3 = QtGui.QLabel(self.replay_gain)
40 | self.label_3.setObjectName(_fromUtf8("label_3"))
41 | self.vboxlayout1.addWidget(self.label_3)
42 | self.metaflac_command = QtGui.QLineEdit(self.replay_gain)
43 | self.metaflac_command.setObjectName(_fromUtf8("metaflac_command"))
44 | self.vboxlayout1.addWidget(self.metaflac_command)
45 | self.label_4 = QtGui.QLabel(self.replay_gain)
46 | self.label_4.setObjectName(_fromUtf8("label_4"))
47 | self.vboxlayout1.addWidget(self.label_4)
48 | self.wvgain_command = QtGui.QLineEdit(self.replay_gain)
49 | self.wvgain_command.setObjectName(_fromUtf8("wvgain_command"))
50 | self.vboxlayout1.addWidget(self.wvgain_command)
51 | self.vboxlayout.addWidget(self.replay_gain)
52 | spacerItem = QtGui.QSpacerItem(263, 21, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
53 | self.vboxlayout.addItem(spacerItem)
54 |
55 | self.retranslateUi(ReplayGainOptionsPage)
56 | QtCore.QMetaObject.connectSlotsByName(ReplayGainOptionsPage)
57 |
58 | def retranslateUi(self, ReplayGainOptionsPage):
59 | self.replay_gain.setTitle(_("Replay Gain"))
60 | self.label.setText(_("Path to VorbisGain:"))
61 | self.label_2.setText(_("Path to MP3Gain:"))
62 | self.label_3.setText(_("Path to metaflac:"))
63 | self.label_4.setText(_("Path to wvgain:"))
64 |
65 |
--------------------------------------------------------------------------------
/plugins/save_and_rewrite_header/save_and_rewrite_header.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*-
3 | #
4 | # This source code is a plugin for MusicBrainz Picard.
5 | # It adds a context menu action to save files and rewrite their header.
6 | # Copyright (C) 2015 Nicolas Cenerario
7 | #
8 | # This program is free software: you can redistribute it and/or modify
9 | # it under the terms of the GNU General Public License as published by
10 | # the Free Software Foundation, either version 3 of the License, or
11 | # (at your option) any later version.
12 | #
13 | # This program is distributed in the hope that it will be useful,
14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 | # GNU General Public License for more details.
17 | #
18 | # You should have received a copy of the GNU General Public License
19 | # along with this program. If not, see .
20 |
21 | from __future__ import unicode_literals
22 |
23 | PLUGIN_NAME = "Save and rewrite header"
24 | PLUGIN_AUTHOR = "Nicolas Cenerario"
25 | PLUGIN_DESCRIPTION = "This plugin adds a context menu action to save files and rewrite their header."
26 | PLUGIN_VERSION = "0.2"
27 | PLUGIN_API_VERSIONS = ["0.9.0", "0.10", "0.15"]
28 | PLUGIN_LICENSE = "GPL-3.0"
29 | PLUGIN_LICENSE_URL = "http://www.gnu.org/licenses/gpl-3.0.txt"
30 |
31 | from _functools import partial
32 | from mutagen import File as MFile
33 | from picard.ui.itemviews import (BaseAction, register_album_action,
34 | register_cluster_action,
35 | register_clusterlist_action,
36 | register_track_action, register_file_action)
37 | from picard.metadata import Metadata
38 | from picard.util import thread
39 |
40 |
41 | class save_and_rewrite_header(BaseAction):
42 |
43 | NAME = "Save and rewrite header"
44 |
45 | def __init__(self):
46 | super(save_and_rewrite_header, self).__init__()
47 | register_file_action(self)
48 | register_track_action(self)
49 | register_album_action(self)
50 | register_cluster_action(self)
51 | register_clusterlist_action(self)
52 |
53 | def callback(self, obj):
54 | def save(pf):
55 | metadata = Metadata()
56 | metadata.copy(pf.metadata)
57 | mf = MFile(pf.filename)
58 | if mf is not None:
59 | mf.delete()
60 | return pf._save_and_rename(pf.filename, metadata)
61 | for f in self.tagger.get_files_from_objects(obj, save=True):
62 | f.set_pending()
63 | thread.run_task(partial(save, f), f._saving_finished,
64 | priority=2, thread_pool=f.tagger.save_thread_pool)
65 |
66 | save_and_rewrite_header()
67 |
--------------------------------------------------------------------------------
/plugins/smart_title_case/smart_title_case.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python2
2 | # -*- coding: utf-8 -*-
3 |
4 | # This is the Smart Title Case plugin for MusicBrainz Picard.
5 | # Copyright (C) 2017 Sophist.
6 | #
7 | # It is based on the Title Case plugin by Javier Kohen
8 | # Copyright 2007 Javier Kohen
9 | #
10 | # This program is free software; you can redistribute it and/or
11 | # modify it under the terms of the GNU General Public License
12 | # as published by the Free Software Foundation; either version 2
13 | # of the License, or (at your option) any later version.
14 | #
15 | # This program is distributed in the hope that it will be useful,
16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 | # GNU General Public License for more details.
19 |
20 | PLUGIN_NAME = "Smart Title Case"
21 | PLUGIN_AUTHOR = u"Sophist based on an earlier plugin by Javier Kohen"
22 | PLUGIN_DESCRIPTION = """
23 | Capitalize First Character In Every Word Of Album/Track Title/Artist.
24 | Leaves words containing embedded uppercase as-is i.e. USA or DoA.
25 | For Artist/AlbumArtist, title cases only artists not join phrases
26 | e.g. The Beatles feat. The Who.
27 | """
28 | PLUGIN_VERSION = "0.1"
29 | PLUGIN_API_VERSIONS = ["1.4.0"]
30 | PLUGIN_LICENSE = "GPL-3.0"
31 | PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-3.0.html"
32 |
33 | import re, unicodedata
34 |
35 | title_tags = ['title', 'album']
36 | artist_tags = [
37 | ('artist', 'artists'),
38 | ('artistsort', '~artists_sort'),
39 | ('albumartist', '~albumartists'),
40 | ('albumartistsort', '~albumartists_sort'),
41 | ]
42 | title_re = re.compile(ur'\w[^-,/\s\u2010\u2011]*', re.UNICODE)
43 |
44 | def match_word(match):
45 | word = match.group(0)
46 | if word == word.lower():
47 | word = word[0].upper() + word[1:]
48 | return word
49 |
50 | def string_title_match(match_word, string):
51 | return title_re.sub(match_word, string)
52 |
53 | def string_cleanup(string, locale="utf-8"):
54 | if not string:
55 | return u""
56 | if not isinstance(string, unicode):
57 | string = string.decode(locale)
58 | # Replace with normalised unicode string
59 | return unicodedata.normalize("NFKC", string)
60 |
61 | def string_title_case(string, locale="utf-8"):
62 | """Title-case a string using a less destructive method than str.title."""
63 | return string_title_match(match_word, string_cleanup(string, locale))
64 |
65 | assert "Make Title Case" == string_title_case("make title case")
66 | assert "Already Title Case" == string_title_case("Already Title Case")
67 | assert "mIxEd cAsE" == string_title_case("mIxEd cAsE")
68 | assert "A" == string_title_case("a")
69 | assert "Apostrophe's Apostrophe's" == string_title_case("apostrophe's apostrophe's")
70 | assert "(Bracketed Text)" == string_title_case("(bracketed text)")
71 | assert "'Single Quotes'" == string_title_case("'single quotes'")
72 | assert '"Double Quotes"' == string_title_case('"double quotes"')
73 | assert "A,B" == string_title_case("a,b")
74 | assert "A-B" == string_title_case("a-b")
75 | assert "A/B" == string_title_case("a/b")
76 |
77 | def artist_title_case(text, artists, artists_upper):
78 | """
79 | Use the array of artists and the joined string
80 | to identify artists to make title case
81 | and the join strings to leave as-is.
82 | """
83 | find = u"^(" + ur")(\s+\S+?\s+)(".join((map(re.escape, map(string_cleanup,artists)))) + u")(.*$)"
84 | replace = "".join([ur"%s\%d" % (a, x*2 + 2) for x, a in enumerate(artists_upper)])
85 | result = re.sub(find, replace, string_cleanup(text), re.UNICODE)
86 | return result
87 |
88 | assert "The Beatles feat. The Who" == artist_title_case(
89 | "the beatles feat. the who",
90 | ["the beatles", "the who"],
91 | ["The Beatles", "The Who"]
92 | )
93 |
94 | # Put this here so that above unit tests can run standalone before getting an import error
95 | from picard import log
96 | from picard.metadata import (
97 | register_track_metadata_processor,
98 | register_album_metadata_processor,
99 | )
100 |
101 | def title_case(tagger, metadata, release, track=None):
102 | for name in title_tags:
103 | if name in metadata:
104 | values = metadata.getall(name)
105 | new_values = [string_title_case(v) for v in values]
106 | if values != new_values:
107 | log.debug("SmartTitleCase: %s: %r replaced with %r", name, values, new_values)
108 | metadata[name] = new_values
109 | for artist_string, artists_list in artist_tags:
110 | if artist_string in metadata and artists_list in metadata:
111 | artist = metadata.getall(artist_string)
112 | artists = metadata.getall(artists_list)
113 | new_artists = map(string_title_case, artists)
114 | new_artist = [artist_title_case(x, artists, new_artists) for x in artist]
115 | if artists != new_artists and artist != new_artist:
116 | log.debug("SmartTitleCase: %s: %s replaced with %s", artist_string, artist, new_artist)
117 | log.debug("SmartTitleCase: %s: %r replaced with %r", artists_list, artists, new_artists)
118 | metadata[artist_string] = new_artist
119 | metadata[artists_list] = new_artists
120 | elif artists != new_artists or artist != new_artist:
121 | if artists != new_artists:
122 | log.warning("SmartTitleCase: %s changed, %s wasn't", artists_list, artist_string)
123 | log.warning("SmartTitleCase: %s: %r changed to %r", artists_list, artists, new_artists)
124 | log.warning("SmartTitleCase: %s: %r unchanged", artist_string, artist)
125 | else:
126 | log.warning("SmartTitleCase: %s changed, %s wasn't", artist_string, artists_list)
127 | log.warning("SmartTitleCase: %s: %r changed to %r", artist_string, artist, new_artist)
128 | log.warning("SmartTitleCase: %s: %r unchanged", artists_list, artists)
129 |
130 | register_track_metadata_processor(title_case)
131 | register_album_metadata_processor(title_case)
132 |
--------------------------------------------------------------------------------
/plugins/sort_multivalue_tags/sort_multivalue_tags.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # This is the Sort Multivalue Tags plugin for MusicBrainz Picard.
4 | # Copyright (C) 2013 Sophist
5 | #
6 | # This program is free software; you can redistribute it and/or
7 | # modify it under the terms of the GNU General Public License
8 | # as published by the Free Software Foundation; either version 2
9 | # of the License, or (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 |
16 | PLUGIN_NAME = u"Sort Multi-Value Tags"
17 | PLUGIN_AUTHOR = u"Sophist"
18 | PLUGIN_DESCRIPTION = u'''
19 | This plugin sorts multi-value tags e.g. Performers alphabetically.
20 | Note: Some multi-value tags are excluded for the following reasons:
21 |
22 | Sequence is important e.g. Artists
23 | The sequence of one tag is linked to the sequence of another e.g. Label and Catalogue number.
24 |
25 | '''
26 | PLUGIN_VERSION = "0.2"
27 | PLUGIN_API_VERSIONS = ["0.15"]
28 | PLUGIN_LICENSE = "GPL-2.0"
29 | PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"
30 |
31 | from picard.metadata import register_track_metadata_processor
32 |
33 | # Define tags where sort order is important
34 | _sort_multivalue_tags_exclude = (
35 | 'artists', '~artists_sort', 'musicbrainz_artistid',
36 | 'albumartists', '~albumartists_sort', 'musicbrainz_albumartistid',
37 | 'work', 'musicbrainz_workid',
38 | 'label', 'catalognumber',
39 | 'country', 'date',
40 | 'releasetype',
41 | )
42 | # Possible future enhancement:
43 | # Sort linked tags e.g. work so that the sequence in related tags e.g. workid retains the relationship between
44 | # e.g. work and workid.
45 |
46 |
47 | def sort_multivalue_tags(tagger, metadata, track, release):
48 |
49 | for tag in metadata.keys():
50 | if tag in _sort_multivalue_tags_exclude:
51 | continue
52 | data = metadata.getall(tag)
53 | if len(data) > 1:
54 | sorted_data = sorted(data)
55 | if data != sorted_data:
56 | metadata.set(tag, sorted_data)
57 |
58 | register_track_metadata_processor(sort_multivalue_tags)
59 |
--------------------------------------------------------------------------------
/plugins/soundtrack/soundtrack.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Copyright © 2015 Samir Benmendil
4 | # This work is free. You can redistribute it and/or modify it under the
5 | # terms of the Do What The Fuck You Want To Public License, Version 2,
6 | # as published by Sam Hocevar. See http://www.wtfpl.net/ for more details.
7 |
8 | PLUGIN_NAME = 'Soundtrack'
9 | PLUGIN_AUTHOR = 'Samir Benmendil'
10 | PLUGIN_LICENSE = 'WTFPL'
11 | PLUGIN_LICENSE_URL = 'http://www.wtfpl.net/'
12 | PLUGIN_DESCRIPTION = '''Sets the albumartist to "Soundtrack" if releasetype is a soundtrack.'''
13 | PLUGIN_VERSION = "0.1"
14 | PLUGIN_API_VERSIONS = ["1.0"]
15 |
16 | from picard.metadata import register_album_metadata_processor
17 |
18 |
19 | def soundtrack(tagger, metadata, release):
20 | if "soundtrack" in metadata["releasetype"]:
21 | metadata["albumartist"] = "Soundtrack"
22 | metadata["albumartistsort"] = "Soundtrack"
23 |
24 | register_album_metadata_processor(soundtrack)
25 |
--------------------------------------------------------------------------------
/plugins/standardise_feat/standardise_feat.py:
--------------------------------------------------------------------------------
1 | PLUGIN_NAME = 'Standardise Feat.'
2 | PLUGIN_AUTHOR = 'Sambhav Kothari'
3 | PLUGIN_DESCRIPTION = 'Standardises "featuring" join phrases for artists to "feat."'
4 | PLUGIN_VERSION = "0.1"
5 | PLUGIN_API_VERSIONS = ["1.4"]
6 |
7 | import re
8 | from picard import log
9 | from picard.metadata import register_track_metadata_processor, register_album_metadata_processor
10 | from picard.plugin import PluginPriority
11 |
12 | _feat_re = re.compile(r" f(ea)?t(\.|uring)? ", re.IGNORECASE)
13 |
14 |
15 | def standardise_feat(artists_str, artists_list):
16 | match_exp = r"(\s*.*\s*)".join((map(re.escape, artists_list)))
17 | try:
18 | join_phrases = re.match(match_exp, artists_str).groups()
19 | except AttributeError:
20 | log.debug("Unable to standardise artists: %r", artists_str)
21 | return artists_str
22 | else:
23 | standardised_join_phrases = [_feat_re.sub(" feat. ", phrase)
24 | for phrase in join_phrases]
25 | # Add a blank string at the end to allow zipping of
26 | # join phrases and artists_list since there is one less join phrase
27 | standardised_join_phrases.append("")
28 | return "".join([artist + join_phrase for artist, join_phrase in
29 | zip(artists_list, standardised_join_phrases)])
30 |
31 |
32 | def standardise_track_artist(tagger, metadata, release, track):
33 | metadata["artist"] = standardise_feat(metadata["artist"], metadata.getall("artists"))
34 | metadata["artistsort"] = standardise_feat(metadata["artistsort"], metadata.getall("~artists_sort"))
35 |
36 |
37 | def standardise_album_artist(tagger, metadata, release):
38 | metadata["albumartist"] = standardise_feat(metadata["albumartist"], metadata.getall("~albumartists"))
39 | metadata["albumartistsort"] = standardise_feat(metadata["albumartistsort"], metadata.getall("~albumartists_sort"))
40 |
41 |
42 | register_track_metadata_processor(standardise_track_artist, priority=PluginPriority.HIGH)
43 | register_album_metadata_processor(standardise_album_artist, priority=PluginPriority.HIGH)
44 |
--------------------------------------------------------------------------------
/plugins/standardise_performers/standardise_performers.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | PLUGIN_NAME = u'Standardise Performers'
4 | PLUGIN_AUTHOR = u'Sophist'
5 | PLUGIN_DESCRIPTION = u'''Splits multi-instrument performer tags into single
6 | instruments and combines names so e.g. (from 10cc by 10cc track 1):
7 |
8 | Performer [acoustic guitar, bass, dobro, electric guitar and tambourine]: Graham Gouldman
9 | Performer [acoustic guitar, electric guitar, grand piano and synthesizer]: Lol Creme
10 | Performer [electric guitar, moog and slide guitar]: Eric Stewart
11 |
12 | becomes:
13 |
14 | Performer [acoustic guitar]: Graham Gouldman; Lol Creme
15 | Performer [bass]: Graham Gouldman
16 | Performer [dobro]: Graham Gouldman
17 | Performer [electric guitar]: Eric Stewart; Graham Gouldman; Lol Creme
18 | Performer [grand piano]: Lol Creme
19 | Performer [moog]: Eric Stewart
20 | Performer [slide guitar]: Eric Stewart
21 | Performer [synthesizer]: Lol Creme
22 | Performer [tambourine]: Graham Gouldman
23 |
24 | '''
25 | PLUGIN_VERSION = '0.2'
26 | PLUGIN_API_VERSIONS = ["0.15.0", "0.15.1", "0.16.0", "1.0.0", "1.1.0", "1.2.0", "1.3.0"]
27 | PLUGIN_LICENSE = "GPL-2.0"
28 | PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"
29 |
30 | import re
31 | from picard import log
32 | from picard.metadata import register_track_metadata_processor
33 |
34 | standardise_performers_split = re.compile(r", | and ").split
35 |
36 |
37 | def standardise_performers(album, metadata, *args):
38 | for key, values in metadata.rawitems():
39 | if not key.startswith('performer:') \
40 | and not key.startswith('~performersort:'):
41 | continue
42 | mainkey, subkey = key.split(':', 1)
43 | if not subkey:
44 | continue
45 | instruments = standardise_performers_split(subkey)
46 | if len(instruments) == 1:
47 | continue
48 | log.debug("%s: Splitting Performer [%s] into separate performers",
49 | PLUGIN_NAME,
50 | subkey,
51 | )
52 | for instrument in instruments:
53 | newkey = '%s:%s' % (mainkey, instrument)
54 | for value in values:
55 | metadata.add_unique(newkey, value)
56 | del metadata[key]
57 |
58 |
59 | try:
60 | from picard.plugin import PluginPriority
61 |
62 | register_track_metadata_processor(standardise_performers,
63 | priority=PluginPriority.HIGH)
64 | except ImportError:
65 | log.warning(
66 | "Running %r plugin on this Picard version may not work as you expect. "
67 | "Any other plugins that run before it will get the old performers "
68 | "rather than the standardized performers.", PLUGIN_NAME
69 | )
70 | register_track_metadata_processor(standardise_performers)
71 |
--------------------------------------------------------------------------------
/plugins/titlecase/titlecase.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Copyright 2007 Javier Kohen
3 | #
4 | # This program is free software; you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License version 2 as
6 | # published by the Free Software Foundation
7 |
8 | PLUGIN_NAME = "Title Case"
9 | PLUGIN_AUTHOR = u"Javier Kohen"
10 | PLUGIN_DESCRIPTION = "Capitalize First Character In Every Word Of A Title"
11 | PLUGIN_VERSION = "0.1"
12 | PLUGIN_API_VERSIONS = ["0.9", "0.10", "0.11", "0.15"]
13 | PLUGIN_LICENSE = "GPL-2.0"
14 | PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"
15 |
16 | import unicodedata
17 |
18 |
19 | def iswbound(char):
20 | """Returns whether the given character is a word boundary."""
21 | category = unicodedata.category(char)
22 | # If it's a space separator or punctuation
23 | return 'Zs' == category or 'Sk' == category or 'P' == category[0]
24 |
25 |
26 | def utitle(string):
27 | """Title-case a string using a less destructive method than str.title."""
28 | new_string = string[0].capitalize()
29 | cap = False
30 | for i in xrange(1, len(string)):
31 | s = string[i]
32 | # Special case apostrophe in the middle of a word.
33 | if s in u"’'" and string[i - 1].isalpha():
34 | cap = False
35 | elif iswbound(s):
36 | cap = True
37 | elif cap and s.isalpha():
38 | cap = False
39 | s = s.capitalize()
40 | else:
41 | cap = False
42 | new_string += s
43 | return new_string
44 |
45 |
46 | def title(string, locale="utf-8"):
47 | """Title-case a string using a less destructive method than str.title."""
48 | if not string:
49 | return u""
50 | # if the string is all uppercase, lowercase it - Erich/Javier
51 | # Lots of Japanese songs use entirely upper-case English titles,
52 | # so I don't like this change... - JoeW
53 | #if string == string.upper(): string = string.lower()
54 | if not isinstance(string, unicode):
55 | string = string.decode(locale)
56 | return utitle(string)
57 |
58 | from picard.metadata import (
59 | register_track_metadata_processor,
60 | register_album_metadata_processor,
61 | )
62 |
63 |
64 | def title_case(tagger, metadata, release, track=None):
65 | for name, value in metadata.rawitems():
66 | if name in ["title", "album", "artist"]:
67 | metadata[name] = [title(x) for x in value]
68 |
69 | register_track_metadata_processor(title_case)
70 | register_album_metadata_processor(title_case)
71 |
--------------------------------------------------------------------------------
/plugins/tracks2clipboard/tracks2clipboard.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | PLUGIN_NAME = u"Copy Cluster to Clipboard"
4 | PLUGIN_AUTHOR = u"Michael Elsdörfer"
5 | PLUGIN_DESCRIPTION = "Exports a cluster's tracks to the clipboard, so it can be copied into the tracklist field on MusicBrainz"
6 | PLUGIN_VERSION = "0.1"
7 | PLUGIN_API_VERSIONS = ["0.9.0", "0.10", "0.15"]
8 |
9 |
10 | from PyQt4 import QtCore, QtGui
11 | from picard.cluster import Cluster
12 | from picard.util import format_time
13 | from picard.ui.itemviews import BaseAction, register_cluster_action
14 |
15 |
16 | class CopyClusterToClipboard(BaseAction):
17 | NAME = "Copy Cluster to Clipboard..."
18 |
19 | def callback(self, objs):
20 | if len(objs) != 1 or not isinstance(objs[0], Cluster):
21 | return
22 | cluster = objs[0]
23 |
24 | artists = set()
25 | for i, file in enumerate(cluster.files):
26 | artists.add(file.metadata["artist"])
27 |
28 | tracks = []
29 | for i, file in enumerate(cluster.files):
30 | try:
31 | i = int(file.metadata["tracknumber"])
32 | except:
33 | i += 1
34 |
35 | if len(artists) > 1:
36 | tracks.append((i, "%s. %s - %s (%s)" % (
37 | i,
38 | file.metadata["title"],
39 | file.metadata["artist"],
40 | format_time(file.metadata.length))))
41 | else:
42 | tracks.append((i, "%s. %s (%s)" % (
43 | i,
44 | file.metadata["title"],
45 | format_time(file.metadata.length))))
46 |
47 | clipboard = QtGui.QApplication.clipboard()
48 | clipboard.setText("\n".join(map(lambda x: x[1], sorted(tracks))))
49 |
50 |
51 | register_cluster_action(CopyClusterToClipboard())
52 |
--------------------------------------------------------------------------------
/plugins/videotools/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2014 Philipp Wolfer
4 | #
5 | # This program is free software; you can redistribute it and/or
6 | # modify it under the terms of the GNU General Public License
7 | # as published by the Free Software Foundation; either version 2
8 | # of the License, or (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program; if not, write to the Free Software
17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 | # 02110-1301, USA.
19 |
20 | PLUGIN_NAME = 'Video tools'
21 | PLUGIN_AUTHOR = 'Philipp Wolfer'
22 | PLUGIN_DESCRIPTION = 'Improves the video support in Picard by adding support for Matroska, WebM, AVI, QuickTime and MPEG files (renaming and fingerprinting only, no tagging) and providing $is_audio() and $is_video() scripting functions.'
23 | PLUGIN_VERSION = "0.1"
24 | PLUGIN_API_VERSIONS = ["1.3.0"]
25 | PLUGIN_LICENSE = "GPL-2.0"
26 | PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"
27 |
28 | from picard.formats import register_format
29 | from picard.script import register_script_function
30 | from picard.plugins.videotools.formats import MatroskaFile, MpegFile, QuickTimeFile, RiffFile
31 | from picard.plugins.videotools.script import is_audio, is_video
32 |
33 |
34 | # Now this is kind of a hack, but Picard won't process registered objects that
35 | # are in a submodule of a plugin. I still want the code to be in separate files.
36 | MatroskaFile.__module__ = MpegFile.__module__ = QuickTimeFile.__module__ = \
37 | RiffFile.__module__ = is_audio.__module__ = is_video.__module__ = \
38 | __name__
39 |
40 | register_format(MatroskaFile)
41 | register_format(MpegFile)
42 | register_format(QuickTimeFile)
43 | register_format(RiffFile)
44 |
45 | register_script_function(is_audio)
46 | register_script_function(is_video)
47 |
--------------------------------------------------------------------------------
/plugins/videotools/enzyme/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # enzyme - Video metadata parser
3 | # Copyright 2011-2012 Antoine Bertin
4 | # Copyright 2003-2006 Thomas Schueppel
5 | # Copyright 2003-2006 Dirk Meyer
6 | #
7 | # This file is part of enzyme.
8 | #
9 | # enzyme is free software; you can redistribute it and/or modify it under
10 | # the terms of the GNU General Public License as published by
11 | # the Free Software Foundation; either version 3 of the License, or
12 | # (at your option) any later version.
13 | #
14 | # enzyme is distributed in the hope that it will be useful,
15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | # GNU General Public License for more details.
18 | #
19 | # You should have received a copy of the GNU General Public License
20 | # along with enzyme. If not, see .
21 | from __future__ import absolute_import
22 | import mimetypes
23 | import os
24 | import sys
25 | from .exceptions import *
26 |
27 |
28 | PARSERS = [('asf', ['video/asf'], ['asf', 'wmv', 'wma']),
29 | ('flv', ['video/flv'], ['flv']),
30 | ('mkv', ['video/x-matroska', 'application/mkv'], ['mkv', 'mka', 'webm']),
31 | ('mp4', ['video/quicktime', 'video/mp4'], ['mov', 'qt', 'mp4', 'mp4a', '3gp', '3gp2', '3g2', 'mk2']),
32 | ('mpeg', ['video/mpeg'], ['mpeg', 'mpg', 'mp4', 'ts']),
33 | ('ogm', ['application/ogg'], ['ogm', 'ogg', 'ogv']),
34 | ('real', ['video/real'], ['rm', 'ra', 'ram']),
35 | ('riff', ['video/avi'], ['wav', 'avi'])
36 | ]
37 |
38 |
39 | def parse(path):
40 | """Parse metadata of the given video
41 |
42 | :param string path: path to the video file to parse
43 | :return: a parser corresponding to the video's mimetype or extension
44 | :rtype: :class:`~enzyme.core.AVContainer`
45 |
46 | """
47 | if not os.path.isfile(path):
48 | raise ValueError('Invalid path')
49 | extension = os.path.splitext(path)[1][1:]
50 | mimetype = mimetypes.guess_type(path)[0]
51 | parser_ext = None
52 | parser_mime = None
53 | for (parser_name, parser_mimetypes, parser_extensions) in PARSERS:
54 | if mimetype in parser_mimetypes:
55 | parser_mime = parser_name
56 | if extension in parser_extensions:
57 | parser_ext = parser_name
58 | parser = parser_mime or parser_ext
59 | if not parser:
60 | raise NoParserError()
61 | mod = __import__(parser, globals=globals(), locals=locals(), fromlist=[], level=-1)
62 | with open(path, 'rb') as f:
63 | p = mod.Parser(f)
64 | return p
65 |
--------------------------------------------------------------------------------
/plugins/videotools/enzyme/exceptions.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # enzyme - Video metadata parser
3 | # Copyright 2011-2012 Antoine Bertin
4 | #
5 | # This file is part of enzyme.
6 | #
7 | # enzyme is free software; you can redistribute it and/or modify it under
8 | # the terms of the GNU General Public License as published by
9 | # the Free Software Foundation; either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # enzyme is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with enzyme. If not, see .
19 | class Error(Exception):
20 | pass
21 |
22 |
23 | class NoParserError(Error):
24 | pass
25 |
26 |
27 | class ParseError(Error):
28 | pass
29 |
--------------------------------------------------------------------------------
/plugins/videotools/enzyme/flv.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # enzyme - Video metadata parser
3 | # Copyright 2011-2012 Antoine Bertin
4 | # Copyright 2003-2006 Dirk Meyer
5 | #
6 | # This file is part of enzyme.
7 | #
8 | # enzyme is free software; you can redistribute it and/or modify it under
9 | # the terms of the GNU General Public License as published by
10 | # the Free Software Foundation; either version 3 of the License, or
11 | # (at your option) any later version.
12 | #
13 | # enzyme is distributed in the hope that it will be useful,
14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 | # GNU General Public License for more details.
17 | #
18 | # You should have received a copy of the GNU General Public License
19 | # along with enzyme. If not, see .
20 | from __future__ import absolute_import
21 | from .exceptions import ParseError
22 | from . import core
23 | import logging
24 | import struct
25 |
26 | __all__ = ['Parser']
27 |
28 |
29 | # get logging object
30 | log = logging.getLogger(__name__)
31 |
32 | FLV_TAG_TYPE_AUDIO = 0x08
33 | FLV_TAG_TYPE_VIDEO = 0x09
34 | FLV_TAG_TYPE_META = 0x12
35 |
36 | # audio flags
37 | FLV_AUDIO_CHANNEL_MASK = 0x01
38 | FLV_AUDIO_SAMPLERATE_MASK = 0x0c
39 | FLV_AUDIO_CODECID_MASK = 0xf0
40 |
41 | FLV_AUDIO_SAMPLERATE_OFFSET = 2
42 | FLV_AUDIO_CODECID_OFFSET = 4
43 | FLV_AUDIO_CODECID = (0x0001, 0x0002, 0x0055, 0x0001)
44 |
45 | # video flags
46 | FLV_VIDEO_CODECID_MASK = 0x0f
47 | FLV_VIDEO_CODECID = ('FLV1', 'MSS1', 'VP60') # wild guess
48 |
49 | FLV_DATA_TYPE_NUMBER = 0x00
50 | FLV_DATA_TYPE_BOOL = 0x01
51 | FLV_DATA_TYPE_STRING = 0x02
52 | FLV_DATA_TYPE_OBJECT = 0x03
53 | FLC_DATA_TYPE_CLIP = 0x04
54 | FLV_DATA_TYPE_REFERENCE = 0x07
55 | FLV_DATA_TYPE_ECMARRAY = 0x08
56 | FLV_DATA_TYPE_ENDOBJECT = 0x09
57 | FLV_DATA_TYPE_ARRAY = 0x0a
58 | FLV_DATA_TYPE_DATE = 0x0b
59 | FLV_DATA_TYPE_LONGSTRING = 0x0c
60 |
61 | FLVINFO = {
62 | 'creator': 'copyright',
63 | }
64 |
65 | class FlashVideo(core.AVContainer):
66 | """
67 | Experimental parser for Flash videos. It requires certain flags to
68 | be set to report video resolutions and in most cases it does not
69 | provide that information.
70 | """
71 | table_mapping = { 'FLVINFO' : FLVINFO }
72 |
73 | def __init__(self, file):
74 | core.AVContainer.__init__(self)
75 | self.mime = 'video/flv'
76 | self.type = 'Flash Video'
77 | data = file.read(13)
78 | if len(data) < 13 or struct.unpack('>3sBBII', data)[0] != 'FLV':
79 | raise ParseError()
80 |
81 | for _ in range(10):
82 | if self.audio and self.video:
83 | break
84 | data = file.read(11)
85 | if len(data) < 11:
86 | break
87 | chunk = struct.unpack('>BH4BI', data)
88 | size = (chunk[1] << 8) + chunk[2]
89 |
90 | if chunk[0] == FLV_TAG_TYPE_AUDIO:
91 | flags = ord(file.read(1))
92 | if not self.audio:
93 | a = core.AudioStream()
94 | a.channels = (flags & FLV_AUDIO_CHANNEL_MASK) + 1
95 | srate = (flags & FLV_AUDIO_SAMPLERATE_MASK)
96 | a.samplerate = (44100 << (srate >> FLV_AUDIO_SAMPLERATE_OFFSET) >> 3)
97 | codec = (flags & FLV_AUDIO_CODECID_MASK) >> FLV_AUDIO_CODECID_OFFSET
98 | if codec < len(FLV_AUDIO_CODECID):
99 | a.codec = FLV_AUDIO_CODECID[codec]
100 | self.audio.append(a)
101 |
102 | file.seek(size - 1, 1)
103 |
104 | elif chunk[0] == FLV_TAG_TYPE_VIDEO:
105 | flags = ord(file.read(1))
106 | if not self.video:
107 | v = core.VideoStream()
108 | codec = (flags & FLV_VIDEO_CODECID_MASK) - 2
109 | if codec < len(FLV_VIDEO_CODECID):
110 | v.codec = FLV_VIDEO_CODECID[codec]
111 | # width and height are in the meta packet, but I have
112 | # no file with such a packet inside. So maybe we have
113 | # to decode some parts of the video.
114 | self.video.append(v)
115 |
116 | file.seek(size - 1, 1)
117 |
118 | elif chunk[0] == FLV_TAG_TYPE_META:
119 | log.info(u'metadata %r', str(chunk))
120 | metadata = file.read(size)
121 | try:
122 | while metadata:
123 | length, value = self._parse_value(metadata)
124 | if isinstance(value, dict):
125 | log.info(u'metadata: %r', value)
126 | if value.get('creator'):
127 | self.copyright = value.get('creator')
128 | if value.get('width'):
129 | self.width = value.get('width')
130 | if value.get('height'):
131 | self.height = value.get('height')
132 | if value.get('duration'):
133 | self.length = value.get('duration')
134 | self._appendtable('FLVINFO', value)
135 | if not length:
136 | # parse error
137 | break
138 | metadata = metadata[length:]
139 | except (IndexError, struct.error, TypeError):
140 | pass
141 | else:
142 | log.info(u'unkown %r', str(chunk))
143 | file.seek(size, 1)
144 |
145 | file.seek(4, 1)
146 |
147 | def _parse_value(self, data):
148 | """
149 | Parse the next metadata value.
150 | """
151 | if ord(data[0]) == FLV_DATA_TYPE_NUMBER:
152 | value = struct.unpack('>d', data[1:9])[0]
153 | return 9, value
154 |
155 | if ord(data[0]) == FLV_DATA_TYPE_BOOL:
156 | return 2, bool(data[1])
157 |
158 | if ord(data[0]) == FLV_DATA_TYPE_STRING:
159 | length = (ord(data[1]) << 8) + ord(data[2])
160 | return length + 3, data[3:length + 3]
161 |
162 | if ord(data[0]) == FLV_DATA_TYPE_ECMARRAY:
163 | init_length = len(data)
164 | num = struct.unpack('>I', data[1:5])[0]
165 | data = data[5:]
166 | result = {}
167 | for _ in range(num):
168 | length = (ord(data[0]) << 8) + ord(data[1])
169 | key = data[2:length + 2]
170 | data = data[length + 2:]
171 | length, value = self._parse_value(data)
172 | if not length:
173 | return 0, result
174 | result[key] = value
175 | data = data[length:]
176 | return init_length - len(data), result
177 |
178 | log.info(u'unknown code: %x. Stop metadata parser', ord(data[0]))
179 | return 0, None
180 |
181 |
182 | Parser = FlashVideo
183 |
--------------------------------------------------------------------------------
/plugins/videotools/enzyme/infos.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # enzyme - Video metadata parser
3 | # Copyright 2011-2012 Antoine Bertin
4 | #
5 | # This file is part of enzyme.
6 | #
7 | # enzyme is free software; you can redistribute it and/or modify it under
8 | # the terms of the GNU General Public License as published by
9 | # the Free Software Foundation; either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # enzyme is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with enzyme. If not, see .
19 | __version__ = '0.2'
20 |
--------------------------------------------------------------------------------
/plugins/videotools/enzyme/real.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # enzyme - Video metadata parser
3 | # Copyright 2011-2012 Antoine Bertin
4 | # Copyright 2003-2006 Thomas Schueppel
5 | # Copyright 2003-2006 Dirk Meyer
6 | #
7 | # This file is part of enzyme.
8 | #
9 | # enzyme is free software; you can redistribute it and/or modify it under
10 | # the terms of the GNU General Public License as published by
11 | # the Free Software Foundation; either version 3 of the License, or
12 | # (at your option) any later version.
13 | #
14 | # enzyme is distributed in the hope that it will be useful,
15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | # GNU General Public License for more details.
18 | #
19 | # You should have received a copy of the GNU General Public License
20 | # along with enzyme. If not, see .
21 | from __future__ import absolute_import
22 | __all__ = ['Parser']
23 |
24 | import struct
25 | import logging
26 | from .exceptions import ParseError
27 | from . import core
28 |
29 | # http://www.pcisys.net/~melanson/codecs/rmff.htm
30 | # http://www.pcisys.net/~melanson/codecs/
31 |
32 | # get logging object
33 | log = logging.getLogger(__name__)
34 |
35 | class RealVideo(core.AVContainer):
36 | def __init__(self, file):
37 | core.AVContainer.__init__(self)
38 | self.mime = 'video/real'
39 | self.type = 'Real Video'
40 | h = file.read(10)
41 | try:
42 | (object_id, object_size, object_version) = struct.unpack('>4sIH', h)
43 | except struct.error:
44 | # EOF.
45 | raise ParseError()
46 |
47 | if not object_id == '.RMF':
48 | raise ParseError()
49 |
50 | file_version, num_headers = struct.unpack('>II', file.read(8))
51 | log.debug(u'size: %d, ver: %d, headers: %d' % \
52 | (object_size, file_version, num_headers))
53 | for _ in range(0, num_headers):
54 | try:
55 | oi = struct.unpack('>4sIH', file.read(10))
56 | except (struct.error, IOError):
57 | # Header data we expected wasn't there. File may be
58 | # only partially complete.
59 | break
60 |
61 | if object_id == 'DATA' and oi[0] != 'INDX':
62 | log.debug(u'INDX chunk expected after DATA but not found -- file corrupt')
63 | break
64 |
65 | (object_id, object_size, object_version) = oi
66 | if object_id == 'DATA':
67 | # Seek over the data chunk rather than reading it in.
68 | file.seek(object_size - 10, 1)
69 | else:
70 | self._read_header(object_id, file.read(object_size - 10))
71 | log.debug(u'%r [%d]' % (object_id, object_size - 10))
72 | # Read all the following headers
73 |
74 |
75 | def _read_header(self, object_id, s):
76 | if object_id == 'PROP':
77 | prop = struct.unpack('>9IHH', s)
78 | log.debug(u'PROP: %r' % prop)
79 | if object_id == 'MDPR':
80 | mdpr = struct.unpack('>H7I', s[:30])
81 | log.debug(u'MDPR: %r' % mdpr)
82 | self.length = mdpr[7] / 1000.0
83 | (stream_name_size,) = struct.unpack('>B', s[30:31])
84 | stream_name = s[31:31 + stream_name_size]
85 | pos = 31 + stream_name_size
86 | (mime_type_size,) = struct.unpack('>B', s[pos:pos + 1])
87 | mime = s[pos + 1:pos + 1 + mime_type_size]
88 | pos += mime_type_size + 1
89 | (type_specific_len,) = struct.unpack('>I', s[pos:pos + 4])
90 | type_specific = s[pos + 4:pos + 4 + type_specific_len]
91 | pos += 4 + type_specific_len
92 | if mime[:5] == 'audio':
93 | ai = core.AudioStream()
94 | ai.id = mdpr[0]
95 | ai.bitrate = mdpr[2]
96 | self.audio.append(ai)
97 | elif mime[:5] == 'video':
98 | vi = core.VideoStream()
99 | vi.id = mdpr[0]
100 | vi.bitrate = mdpr[2]
101 | self.video.append(vi)
102 | else:
103 | log.debug(u'Unknown: %r' % mime)
104 | if object_id == 'CONT':
105 | pos = 0
106 | (title_len,) = struct.unpack('>H', s[pos:pos + 2])
107 | self.title = s[2:title_len + 2]
108 | pos += title_len + 2
109 | (author_len,) = struct.unpack('>H', s[pos:pos + 2])
110 | self.artist = s[pos + 2:pos + author_len + 2]
111 | pos += author_len + 2
112 | (copyright_len,) = struct.unpack('>H', s[pos:pos + 2])
113 | self.copyright = s[pos + 2:pos + copyright_len + 2]
114 | pos += copyright_len + 2
115 | (comment_len,) = struct.unpack('>H', s[pos:pos + 2])
116 | self.comment = s[pos + 2:pos + comment_len + 2]
117 |
118 |
119 | Parser = RealVideo
120 |
--------------------------------------------------------------------------------
/plugins/videotools/enzyme/strutils.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # enzyme - Video metadata parser
3 | # Copyright 2011-2012 Antoine Bertin
4 | # Copyright 2006-2009 Dirk Meyer
5 | # Copyright 2006-2009 Jason Tackaberry
6 | #
7 | # This file is part of enzyme.
8 | #
9 | # enzyme is free software; you can redistribute it and/or modify it under
10 | # the terms of the GNU General Public License as published by
11 | # the Free Software Foundation; either version 3 of the License, or
12 | # (at your option) any later version.
13 | #
14 | # enzyme is distributed in the hope that it will be useful,
15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | # GNU General Public License for more details.
18 | #
19 | # You should have received a copy of the GNU General Public License
20 | # along with enzyme. If not, see .
21 | __all__ = ['ENCODING', 'str_to_unicode', 'unicode_to_str']
22 |
23 | import locale
24 |
25 | # find the correct encoding
26 | try:
27 | ENCODING = locale.getdefaultlocale()[1]
28 | ''.encode(ENCODING)
29 | except (UnicodeError, TypeError):
30 | ENCODING = 'latin-1'
31 |
32 |
33 | def str_to_unicode(s, encoding=None):
34 | """
35 | Attempts to convert a string of unknown character set to a unicode
36 | string. First it tries to decode the string based on the locale's
37 | preferred encoding, and if that fails, fall back to UTF-8 and then
38 | latin-1. If all fails, it will force encoding to the preferred
39 | charset, replacing unknown characters. If the given object is no
40 | string, this function will return the given object.
41 | """
42 | if not type(s) == str:
43 | return s
44 |
45 | if not encoding:
46 | encoding = ENCODING
47 |
48 | for c in [encoding, "utf-8", "latin-1"]:
49 | try:
50 | return s.decode(c)
51 | except UnicodeDecodeError:
52 | pass
53 |
54 | return s.decode(encoding, "replace")
55 |
56 |
57 | def unicode_to_str(s, encoding=None):
58 | """
59 | Attempts to convert a unicode string of unknown character set to a
60 | string. First it tries to encode the string based on the locale's
61 | preferred encoding, and if that fails, fall back to UTF-8 and then
62 | latin-1. If all fails, it will force encoding to the preferred
63 | charset, replacing unknown characters. If the given object is no
64 | unicode string, this function will return the given object.
65 | """
66 | if not type(s) == unicode:
67 | return s
68 |
69 | if not encoding:
70 | encoding = ENCODING
71 |
72 | for c in [encoding, "utf-8", "latin-1"]:
73 | try:
74 | return s.encode(c)
75 | except UnicodeDecodeError:
76 | pass
77 |
78 | return s.encode(encoding, "replace")
79 |
--------------------------------------------------------------------------------
/plugins/videotools/formats.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2014 Philipp Wolfer
4 | #
5 | # This program is free software; you can redistribute it and/or
6 | # modify it under the terms of the GNU General Public License
7 | # as published by the Free Software Foundation; either version 2
8 | # of the License, or (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program; if not, write to the Free Software
17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 | # 02110-1301, USA.
19 |
20 | from __future__ import absolute_import
21 | # -*- coding: utf-8 -*-
22 | #
23 | # Copyright (C) 2014 Philipp Wolfer
24 | #
25 | # This program is free software; you can redistribute it and/or
26 | # modify it under the terms of the GNU General Public License
27 | # as published by the Free Software Foundation; either version 2
28 | # of the License, or (at your option) any later version.
29 | #
30 | # This program is distributed in the hope that it will be useful,
31 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
32 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
33 | # GNU General Public License for more details.
34 | #
35 | # You should have received a copy of the GNU General Public License
36 | # along with this program; if not, write to the Free Software
37 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
38 | # 02110-1301, USA.
39 |
40 | from . import enzyme
41 | from picard import log
42 | from picard.file import File
43 | from picard.formats import register_format
44 | from picard.formats.wav import WAVFile
45 | from picard.metadata import Metadata
46 |
47 |
48 | class EnzymeFile(File):
49 |
50 | def _load(self, filename):
51 | log.debug("Loading file %r", filename)
52 | metadata = Metadata()
53 | self._add_path_to_metadata(metadata)
54 |
55 | try:
56 | parser = enzyme.parse(filename)
57 | log.debug("Metadata for %s:\n%s", filename, unicode(parser))
58 | self._convertMetadata(parser, metadata)
59 | except Exception as err:
60 | log.error("Could not parse file %r: %r", filename, err)
61 |
62 | return metadata
63 |
64 | def _convertMetadata(self, parser, metadata):
65 | metadata['~format'] = parser.type
66 |
67 | if parser.title:
68 | metadata["title"] = parser.title
69 |
70 | if parser.artist:
71 | metadata["artist"] = parser.artist
72 |
73 | if parser.trackno:
74 | parts = parser.trackno.split("/")
75 | metadata["tracknumber"] = parts[0]
76 | if len(parts) > 1:
77 | metadata["totaltracks"] = parts[1]
78 |
79 | if parser.encoder:
80 | metadata["encodedby"] = parser.encoder
81 |
82 | if parser.video[0]:
83 | video = parser.video[0]
84 | metadata["~video"] = True
85 |
86 | if parser.audio[0]:
87 | audio = parser.audio[0]
88 | if audio.channels:
89 | metadata["~channels"] = audio.channels
90 |
91 | if audio.samplerate:
92 | metadata["~sample_rate"] = audio.samplerate
93 |
94 | if audio.language:
95 | metadata["language"] = audio.language
96 |
97 | if parser.length:
98 | metadata.length = parser.length * 1000
99 | elif video and video.length:
100 | metadata.length = parser.video[0].length * 1000
101 |
102 | def _save(self, filename, metadata):
103 | log.debug("Saving file %r", filename)
104 | pass
105 |
106 |
107 | class MatroskaFile(EnzymeFile):
108 | EXTENSIONS = [".mka", ".mkv", ".webm"]
109 | NAME = "Matroska"
110 |
111 |
112 | class MpegFile(EnzymeFile):
113 | EXTENSIONS = [".mpg", ".mpeg"]
114 | NAME = "MPEG"
115 |
116 |
117 | class RiffFile(EnzymeFile):
118 | EXTENSIONS = [".avi"]
119 | NAME = "RIFF"
120 |
121 |
122 | class QuickTimeFile(EnzymeFile):
123 | EXTENSIONS = [".mov", ".qt"]
124 | NAME = "QuickTime"
125 |
--------------------------------------------------------------------------------
/plugins/videotools/script.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Copyright (C) 2014 Philipp Wolfer
4 | #
5 | # This program is free software; you can redistribute it and/or
6 | # modify it under the terms of the GNU General Public License
7 | # as published by the Free Software Foundation; either version 2
8 | # of the License, or (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program; if not, write to the Free Software
17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 | # 02110-1301, USA.
19 |
20 | VIDEO_EXTENSIONS = ['m4v', 'wmv', 'ogv', 'oggtheora']
21 |
22 |
23 | def is_video(parser):
24 | """Returns true, if the file processed is a video file."""
25 | if parser.context['~video'] or parser.context['~extension'] in VIDEO_EXTENSIONS:
26 | return "1"
27 | else:
28 | return ""
29 |
30 |
31 | def is_audio(parser):
32 | """Returns true, if the file processed is an audio file."""
33 | if is_video(parser) == "1":
34 | return ""
35 | else:
36 | return "1"
37 |
--------------------------------------------------------------------------------
/plugins/viewvariables/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | PLUGIN_NAME = u'View script variables'
4 | PLUGIN_AUTHOR = u'Sophist'
5 | PLUGIN_DESCRIPTION = u'''Display a dialog box listing the metadata variables for the track / file.'''
6 | PLUGIN_VERSION = '0.5'
7 | PLUGIN_API_VERSIONS = ['1.0']
8 | PLUGIN_LICENSE = "GPL-2.0"
9 | PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"
10 |
11 | from PyQt4 import QtGui, QtCore
12 | try:
13 | from picard.util.tags import PRESERVED_TAGS
14 | except ImportError:
15 | from picard.file import File
16 | PRESERVED_TAGS = File._default_preserved_tags
17 |
18 | from picard.file import File
19 | from picard.track import Track
20 | from picard.ui.itemviews import BaseAction, register_file_action, register_track_action
21 | from picard.plugins.viewvariables.ui_variables_dialog import Ui_VariablesDialog
22 |
23 |
24 | class ViewVariables(BaseAction):
25 | NAME = 'View script variables'
26 |
27 | def callback(self, objs):
28 | obj = objs[0]
29 | files = self.tagger.get_files_from_objects(objs)
30 | if files:
31 | obj = files[0]
32 | dialog = ViewVariablesDialog(obj)
33 | dialog.exec_()
34 |
35 |
36 | class ViewVariablesDialog(QtGui.QDialog):
37 |
38 | def __init__(self, obj, parent=None):
39 | QtGui.QDialog.__init__(self, parent)
40 | self.ui = Ui_VariablesDialog()
41 | self.ui.setupUi(self)
42 | self.ui.buttonBox.accepted.connect(self.accept)
43 | self.ui.buttonBox.rejected.connect(self.reject)
44 | metadata = obj.metadata
45 | if isinstance(obj, File):
46 | self.setWindowTitle(_("File: %s") % obj.base_filename)
47 | elif isinstance(obj, Track):
48 | tn = metadata['tracknumber']
49 | if len(tn) == 1:
50 | tn = u"0" + tn
51 | self.setWindowTitle(_("Track: %s %s ") % (tn, metadata['title']))
52 | else:
53 | self.setWindowTitle(_("Variables"))
54 | self._display_metadata(metadata)
55 |
56 | def _display_metadata(self, metadata):
57 | keys = metadata.keys()
58 | keys.sort(key=lambda key:
59 | '0' + key if key in PRESERVED_TAGS and key.startswith('~') else
60 | '1' + key if key.startswith('~') else
61 | '2' + key
62 | )
63 | media = hidden = album = False
64 | table = self.ui.metadata_table
65 | key_example, value_example = self.get_table_items(table, 0)
66 | self.key_flags = key_example.flags()
67 | self.value_flags = value_example.flags()
68 | table.setRowCount(len(keys) + 3)
69 | i = 0
70 | for key in keys:
71 | if key in PRESERVED_TAGS and key.startswith('~'):
72 | if not media:
73 | self.add_separator_row(table, i, _("File variables"))
74 | i += 1
75 | media = True
76 | elif key.startswith('~'):
77 | if not hidden:
78 | self.add_separator_row(table, i, _("Hidden variables"))
79 | i += 1
80 | hidden = True
81 | else:
82 | if not album:
83 | self.add_separator_row(table, i, _("Tag variables"))
84 | i += 1
85 | album = True
86 |
87 | key_item, value_item = self.get_table_items(table, i)
88 | i += 1
89 | key_item.setText(u"_" + key[1:] if key.startswith('~') else key)
90 | if key in metadata:
91 | value = dict.get(metadata, key)
92 | if len(value) == 1 and value[0] != '':
93 | value = value[0]
94 | else:
95 | value = repr(value)
96 | value_item.setText(value)
97 |
98 | def add_separator_row(self, table, i, title):
99 | key_item, value_item = self.get_table_items(table, i)
100 | font = key_item.font()
101 | font.setBold(True)
102 | key_item.setFont(font)
103 | key_item.setText(title)
104 |
105 | def get_table_items(self, table, i):
106 | key_item = table.item(i, 0)
107 | value_item = table.item(i, 1)
108 | if not key_item:
109 | key_item = QtGui.QTableWidgetItem()
110 | key_item.setFlags(self.key_flags)
111 | table.setItem(i, 0, key_item)
112 | if not value_item:
113 | value_item = QtGui.QTableWidgetItem()
114 | value_item.setFlags(self.value_flags)
115 | table.setItem(i, 1, value_item)
116 | return key_item, value_item
117 |
118 | vv = ViewVariables()
119 | register_file_action(vv)
120 | register_track_action(vv)
121 |
--------------------------------------------------------------------------------
/plugins/viewvariables/ui_variables_dialog.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Form implementation generated from reading ui file 'ui\variables_dialog.ui'
4 | #
5 | # Created: Wed Mar 26 06:58:04 2014
6 | # by: PyQt4 UI code generator 4.10.3
7 | #
8 | # WARNING! All changes made in this file will be lost!
9 |
10 | from PyQt4 import QtCore, QtGui
11 |
12 | try:
13 | _fromUtf8 = QtCore.QString.fromUtf8
14 | except AttributeError:
15 | def _fromUtf8(s):
16 | return s
17 |
18 | try:
19 | _encoding = QtGui.QApplication.UnicodeUTF8
20 | def _translate(context, text, disambig):
21 | return QtGui.QApplication.translate(context, text, disambig, _encoding)
22 | except AttributeError:
23 | def _translate(context, text, disambig):
24 | return QtGui.QApplication.translate(context, text, disambig)
25 |
26 | class Ui_VariablesDialog(object):
27 | def setupUi(self, VariablesDialog):
28 | VariablesDialog.setObjectName(_fromUtf8("VariablesDialog"))
29 | VariablesDialog.resize(600, 450)
30 | self.verticalLayout = QtGui.QVBoxLayout(VariablesDialog)
31 | self.verticalLayout.setObjectName(_fromUtf8("verticalLayout"))
32 | self.metadata_table = QtGui.QTableWidget(VariablesDialog)
33 | self.metadata_table.setAutoFillBackground(False)
34 | self.metadata_table.setSelectionMode(QtGui.QAbstractItemView.ContiguousSelection)
35 | self.metadata_table.setRowCount(1)
36 | self.metadata_table.setColumnCount(2)
37 | self.metadata_table.setObjectName(_fromUtf8("metadata_table"))
38 | item = QtGui.QTableWidgetItem()
39 | font = QtGui.QFont()
40 | font.setBold(True)
41 | font.setWeight(75)
42 | item.setFont(font)
43 | self.metadata_table.setHorizontalHeaderItem(0, item)
44 | item = QtGui.QTableWidgetItem()
45 | font = QtGui.QFont()
46 | font.setBold(True)
47 | font.setWeight(75)
48 | item.setFont(font)
49 | self.metadata_table.setHorizontalHeaderItem(1, item)
50 | item = QtGui.QTableWidgetItem()
51 | item.setFlags(QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsEnabled)
52 | self.metadata_table.setItem(0, 0, item)
53 | item = QtGui.QTableWidgetItem()
54 | item.setFlags(QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsEnabled)
55 | self.metadata_table.setItem(0, 1, item)
56 | self.metadata_table.horizontalHeader().setDefaultSectionSize(150)
57 | self.metadata_table.horizontalHeader().setSortIndicatorShown(False)
58 | self.metadata_table.horizontalHeader().setStretchLastSection(True)
59 | self.metadata_table.verticalHeader().setVisible(False)
60 | self.metadata_table.verticalHeader().setDefaultSectionSize(20)
61 | self.metadata_table.verticalHeader().setMinimumSectionSize(20)
62 | self.verticalLayout.addWidget(self.metadata_table)
63 | self.buttonBox = QtGui.QDialogButtonBox(VariablesDialog)
64 | self.buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Cancel|QtGui.QDialogButtonBox.Ok)
65 | self.buttonBox.setObjectName(_fromUtf8("buttonBox"))
66 | self.verticalLayout.addWidget(self.buttonBox)
67 |
68 | self.retranslateUi(VariablesDialog)
69 | QtCore.QMetaObject.connectSlotsByName(VariablesDialog)
70 |
71 | def retranslateUi(self, VariablesDialog):
72 | item = self.metadata_table.horizontalHeaderItem(0)
73 | item.setText(_("Variable"))
74 | item = self.metadata_table.horizontalHeaderItem(1)
75 | item.setText(_("Value"))
76 | __sortingEnabled = self.metadata_table.isSortingEnabled()
77 | self.metadata_table.setSortingEnabled(False)
78 | self.metadata_table.setSortingEnabled(__sortingEnabled)
79 |
80 |
--------------------------------------------------------------------------------
/plugins/viewvariables/variables_dialog.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | VariablesDialog
4 |
5 |
6 |
7 | 0
8 | 0
9 | 600
10 | 450
11 |
12 |
13 |
14 | -
15 |
16 |
17 | false
18 |
19 |
20 | QAbstractItemView::ContiguousSelection
21 |
22 |
23 | 1
24 |
25 |
26 | 2
27 |
28 |
29 | 150
30 |
31 |
32 | false
33 |
34 |
35 | true
36 |
37 |
38 | false
39 |
40 |
41 | 20
42 |
43 |
44 | 20
45 |
46 |
47 |
48 |
49 | Variable
50 |
51 |
52 |
53 | 75
54 | true
55 |
56 |
57 |
58 |
59 |
60 | Value
61 |
62 |
63 |
64 | 75
65 | true
66 |
67 |
68 |
69 | -
70 |
71 |
72 |
73 |
74 | ItemIsSelectable|ItemIsEnabled
75 |
76 |
77 | -
78 |
79 | ItemIsSelectable|ItemIsEnabled
80 |
81 |
82 |
83 |
84 | -
85 |
86 |
87 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok
88 |
89 |
90 |
91 |
92 |
93 |
94 | buttonBox
95 |
96 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/test.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 | import os
3 | import glob
4 | import sys
5 | import unittest
6 |
7 | # python 2 & 3 compatibility
8 | try:
9 | basestring
10 | except NameError:
11 | basestring = str
12 |
13 | from generate import *
14 |
15 | # The file that contains json data
16 | plugin_file = "plugins.json"
17 |
18 | # The directory which contains plugin files
19 | plugin_dir = "plugins"
20 |
21 | if sys.version_info[:2] == (2, 6):
22 | def assertIsInstance(self, obj, cls, msg=None):
23 | if not isinstance(obj, cls):
24 | self.fail('%s is not an instance of %r' % (repr(obj), cls))
25 |
26 | unittest.TestCase.assertIsInstance = assertIsInstance
27 |
28 |
29 | class GenerateTestCase(unittest.TestCase):
30 |
31 | """Run tests"""
32 |
33 | def test_generate_json(self):
34 | """
35 | Generates the json data from all the plugins
36 | and asserts that all plugins are accounted for.
37 | """
38 |
39 | print("\n#########################################\n")
40 |
41 | build_json()
42 |
43 | # Load the json file
44 | with open(plugin_file, "r") as in_file:
45 | plugin_json = json.load(in_file)["plugins"]
46 |
47 | # All top level directories in plugin_dir
48 | plugin_folders = next(os.walk(plugin_dir))[1]
49 |
50 | # Number of entries in the json should be equal to the
51 | # number of folders in plugin_dir
52 | self.assertEqual(len(plugin_json), len(plugin_folders))
53 |
54 | def test_generate_zip(self):
55 | """
56 | Generates zip files for all folders and asserts
57 | that all folders are accounted for.
58 | """
59 |
60 | print("\n\n#########################################\n")
61 |
62 | zip_files()
63 |
64 | # All zip files in plugin_dir
65 | plugin_zips = glob.glob(os.path.join(plugin_dir, "*.zip"))
66 |
67 | # All top level directories in plugin_dir
68 | plugin_folders = next(os.walk(plugin_dir))[1]
69 |
70 | # Number of folders should be equal to number of zips
71 | self.assertEqual(len(plugin_zips), len(plugin_folders))
72 |
73 | def test_valid_json(self):
74 | """
75 | Asserts that the json data contains all the fields
76 | for all the plugins.
77 | """
78 |
79 | print("\n#########################################\n")
80 |
81 | build_json()
82 |
83 | # Load the json file
84 | with open(plugin_file, "r") as in_file:
85 | plugin_json = json.load(in_file)["plugins"]
86 |
87 | # All plugins should contain all required fields
88 | for module_name, data in plugin_json.items():
89 | self.assertIsInstance(data['name'], basestring)
90 | self.assertIsInstance(data['api_versions'], list)
91 | self.assertIsInstance(data['author'], basestring)
92 | self.assertIsInstance(data['description'], basestring)
93 | self.assertIsInstance(data['version'], basestring)
94 |
95 | if __name__ == '__main__':
96 | unittest.main()
97 |
--------------------------------------------------------------------------------