From c44df5e4822394e10bf12bb248ee0d39703b8e32 Mon Sep 17 00:00:00 2001
From: Mark Evens
+
+
+ If you want to be more selective as to what is included in host tags, then disable this option and use the tag mapping section to get the data from the hidden variables. If you want to add arrangers as composers, do so in the tag mapping section also.
+
+ (Note that Picard does not normally pick up all arrangers, but that the plugin will do so, provided the "Works and parts" section is run.)
+
+ "Name album as 'Composer Last Name(s): Album Name'" will add the composer(s) last name(s) before the album name, if they are listed as album artists. If there is more than one composer, they will be listed in the descending order of the length of their music on the release. MusicBrainz style is to exclude the composer name unless it is actually part of the album name, but it can be useful to add it for library organisation. The default is checked.
+
+ "Do not write 'lyricist' tag if no vocal performers". Hopefully self-evident. This applies to both the Picard 'lyricist' tag and the related internal plugin hidden variables '\_cwp\_lyricists' etc.
+
+ Note that the plugin will search for lyricists at all work levels (bottom up), but will stop after finding the first one (unless that was just a translator).
+
+ "Do not include attributes in an instrument type" (previously just referred to the attribute 'solo'). MusicBrainz permits the use of "solo", "guest" and "additional" as instrument attributes although, for classical music, its use should be fairly rare - usually only if explicitly stated as a "solo" on the the sleevenotes. Classical Extras provides the option to exclude these attributes (the default), but you may wish to enable them for certain releases or non-Classical / cross-over releases.
+
+ "Annotations": The chosen text will be used to annotate the artist type within the host tag (see table above for host tags), but only if "Modify host tags" is selected.
+
+ Please note that the use of the word "master" is the MusicBrainz term and is not intended to be gender-specific. Users can specify whatever text they please.
+
+5. "Lyrics". **Please note that this section operates on the underlying input file tags, not the Picard-generated tags (MusicBrainz does not have lyrics)**
+ Sometimes "lyrics" tags can contain album notes (repeated for every track in an album) as well as track notes and lyrics. This section will filter out the common text for a release and place it in a different tag from the text which is unique to each track.
+
+ "Split lyrics tag": enables this section.
+
+ "Incoming lyrics tag": The name of the lyrics file tag in the input file (normally just 'lyrics').
+
+ "Tag for album notes": The name of the tag where common text should be placed.
+
+ "Tag for track notes": The name of the tag where notes/lyrics unique to a track should be placed.
+
+ Note that if the 'output' tags are not specified, then internal 'hidden' variables will still be available for use in the tag-mapping section (called album\_notes and track\_notes).
+
+## Work and parts tab
+
+There six coloured sections as shown in the screen print below:
+
+![Works and parts options](https://music.highmossergate.co.uk/classical-extras-screenshots/work-parts/)
+
+1. "Include all work levels" should be selected otherwise this section will not run. This is the default.
+
+ "Include collection relationships" (selected by default) will include parent works where the relationship has the attribute 'part of collection'. See [Discussion](https://community.metabrainz.org/t/levels-in-the-structure-of-works/293047/109) for the background to this. Note that only "work" entity types will be included, not "series" entities. If this option is changed, it will not take effect on releases already loaded in Picard - you will need to quit and restart. PLEASE BE CONSISTENT and do not use different options on albums with the same works, otherwise you may not get what you want.
+
+ "Use cache (if available)" prevents excessive look-ups of the MB database. Every look-up of a work needs to be performed separately (hopefully the MB database might make this easier some day). Network usage constraints by MB means that each look-up takes a minimum of 1 second. Once a release has been looked-up, the works are retained in cache, significantly reducing the time required if, say, the options are changed and the data refreshed. However, if the user edits the works in the MB database then the cache will need to be turned off temporarily for the refresh to find the new/changed works. Also some types of work (e.g. arrangements) will require a full look-up if options have been changed. **Do not leave this option turned off** as it will make the plugin slower and may cause problems. This option will always be set on when Picard is started, regardless of how it was left when it was last closed.
+
+2. "Tagging style". This section determines how the hierarchy of works will be sourced.
+
+ * **Works source**: There are 3 options for determing the principal source of the works metadata
+ - "Use only metadata from title text". The plugin will attempt to extract the hierarchy of works from the track title by looking for repetitions and patterns, using the work structure in MusicBrainz as a guide. If the title does not contain all the work names in the hierarchy then obviously this will limit what can be provided.
+ - "Use only metadata from canonical works". The names from the hierarchy in the MB database will be used. Assuming the work is correctly entered in MB, this should provide all the data. However the text may differ from the track titles and will be the same for all recordings. It may also be in the language of the composer whereas the titles will probably be in the language of the release. (This language issue can also be addressed by using aliases - see below).
+ - "Use canonical work metadata enhanced with title text". This supplements the canonical data with text from the titles **where it is significantly different**. The supplementary title data will be in curly brackets. This is clearly the most complete metadata style of the three but may lead to long descriptions. It is particularly useful for providing translations - see image below for an example (using the Muso library manager). In this example, title text that is similar to that in the canonical text has been eliminated to make the text shorter - the mannr of doing this is controlled by settings on the Advanced tab.
+
+ ![Respighi](https://music.highmossergate.co.uk/classical-extras-screenshots/respighi/)
+
+ * **Source of canonical work text**. Where either of the second two options above are chosen, there is a further choice to be made:
+ - "Full MusicBrainz work hierarchy". The names of each level of work are used to populate the relevant tags. E.g. if ""Concert Fantasy for Piano and Orchestra, op. 56: I. Quasi Rondo" (level 0) is part of "Concert Fantasia, op. 56" (level 1) then that is how they will appear, since there is no repetition of text between parent and child. So, while accurate, this option might sometimes be rather verbose.
+ - "Consistent with lowest level work description". The names of the level 0 work are used to populate the relevant tags. So, in the above example, "Concert Fantasy for Piano and Orchestra, op. 56" will be shown as the work and "I. Quasi Rondo" will be shown as the movement. Sometimes this may look better, but not always, **particularly if the level 0 work name does not contain all the parent work detail**. If the full structure is not implicit in the level 0 name then a warning will be logged and written to the "warning" tag.
+
+ **Version 2.0 update**: the second option is needed less often now as there is a more sophisticated matching algorithm for the canonical work names. Text may be eliminated at places other than the start, and synonyms may be used to achieve greater matching. These options are set on the Advanced tab. Setting "Removal of common text between parent and child works" to 2 (the default) and including "Fantasia" as a synonym of "Fantasy" yields the following result:
+ Work: "Concert Fantasia, op. 56", Movement: "for Piano and Orchestra, … : I. Quasi Rondo"
+ This still repeats "for Piano and Orchestra" for each movement as this text is in level 0, not level 1 (where it only appears as disambiguation). Arguably the best way to fix this is to have consistent work names in MB. (Of course, this specific example may have been fixed in MB by now, but the principle still holds). The strategy below has been updated to reflect this
+
+ **Strategy for setting style:** *It is suggested that you start with "extended/enhanced" style and the "Full MusicBrainz work hierarchy" as the source (this is the default) and tweak the advanced settings if necessary. If this does not give acceptable results, try switching to "Consistent with lowest level work description". If the "enhanced" details in curly brackets (from the track title) give odd results then, again, try tweaking the advanced settings (see later section) or switch the style to "canonical works" only. Any remaining oddities are probably in the MusicBrainz data, which may require editing.*
+
+ * **"Attempt to get works and movement info from title if there are no work relationships? (Requires title in form "work: movement")"**.
+ Pretty much what it says. It may be that the track is classical, but no work relationships exist in MusicBrainz. In this case, Classical Extras will attempt to infer work and movement from the title, provided they are separated by ": " (which is the Classical Style Guideline).
+ In this case, the other tag style settings are irrelevant. Note that if there is no related work, then there will not be a composer metadata item in MusicBrainz. However, you can use tag mapping to set this or (better) use Muso (or and XML reference file) to determine classical composers (see Genres section).
+
+3. "Aliases"
+
+ "Replace work names by aliases" will use **primary** aliases for the chosen locale instead of standard MusicBrainz work names. To choose the locale, use the drop-down under "translate artist names" in the main Picard Options-->Metadata page. Note that this option is not saved as a file tag since, if different choices are made for different releases, different work names may be stored and therefore cannot be grouped together in your player/library manager. The sub-options then allow either the replacement of all work names, where a primary alias exists, just the replacement of work names which are in non-Latin script, or only replace those which are flagged with user "Folksonomy" tags. The tag text needs to be included in the text box, in which case flagged works will be 'aliased' as well as non-Latin script works, if the second sub-option is chosen. Note that the tags may either be anyone's tags ("Look in all tags") or the user's own tags. If selecting "Look in user's own tags only" you **must** be logged in to your MusicBrainz user account (in the Picard Options->General page), otherwise repeated dialogue boxes may be generated and you may need to force restart Picard.
+
+4. "Tags to create" sets the names of the tags that will be created from the sources described above. All these tags will be blanked before filling as specified. Tags specified against more than one source will have later sources appended in the sequence specified, separated by separators as specified.
+
+ * **Work tags**:
+ - "Tags for Work - for software with 2-level capability". Some software (notably Muso) can display a 2-level work hierarchy as well as the work-movement hierarchy. This tag can be use to store the 2-level work name (a double colon :: is used to separate the levels within the tag).
+ - "Tags for Work - for software with 1-level capability". Software which can display a movement and work (but no higher levels) could use any tags specified here. Note that if there are multiple work levels, the intermediate levels will not be tagged. Users wanting all the information should use the tags from the previous option (but it may cause some breaks in the display if levels change) - alternatively the missing work levels can be included in a movement tag (see below).
+ - "Tags for top-level (canonical) work". This is the top-level work held in MB. This can be useful for cataloguing and searching (if the library software is capable).
+
+ * **Movement/Part tags**:
+ (a) "Tags for (computed) movement number". This is not necessarily the embedded movt/part number, but is the sequence number of the movement within its parent work **on the current release**.
+ (For these purposes, the "parent work" is the highest level work of which the track/movement is a a part but which is not a collection)
+ (b) "Tags for (computed) total number of movements". This will be the total number of movements in the parent work as numbered above.
+ (c) "Tags for Movement - excluding embedded movt/part numbers". As below, but without the movement part/number prefix (if applicable)
+ (d) "Tags for Movement - including embedded movt/part numbers". This tag(s) will contain the full lowest-level part name extracted from the lowest-level work name, according to the chosen tagging style.
+ For options (c) and (d), the tags can either be filled "for use with multi-level work tags" or "for use with 1-level work tags (intermediate works will prefix movement)" - or different tags for each column. The latter option will include any intermediate work levels which are missing from a single-level work tag. Use different tag names for these, from the multi-level version, otherwise both versions will be appended, creating a multi-valued tag (a warning will be given).
+ The default tags for (a), (b), and (c) are movementnumber, movementtotal and movement respectively - these are the standard Picard tags for these items.
+ Note that if a tag is included in (a) and either of (c) or (d), the movement number will be prepended at the beginning of the tag, followed by the selected separator. For more complex combinations, use the Tag Mapping tab (e.g. movementnumber + \ of + movementtotal).
+ If you wish to use items (a) and (b) in the tag-mapping section without populating the Picard standard tags, then use the hidden variables movt_num and movt_tot.
+ For more details, see the hidden variables section.
+
+ **Strategy for setting tags:** *It is suggested that initially you just use the multi-level work tag and related movement tags, even if your software only has a single-level work capability. This may result in work names being repeated in work headings, but may look better than the alternative of having work names repeated in movement names. This is the default.*
+
+ *If this does not look good you can then compare it with the alternative approach and change as required for specific releases. If your software does not have any "work" capability, then you can still get the full work details by, for example, specifying "title" as both a work and a movement tag.*
+
+5. "Partial recordings, arrangements and medleys" gives various options where recordings are not just simply of a named complete work. These only apply if one of the two "canonical work" styles is in operation (i.e. not if "Use only metadata from title text" is selected).
+
+ * **Partial recordings**:
+ If this option is selected, partial recordings will be treated as a sub-part of the whole recording and will have the related text (in the adjacent box) included in its name. Note that this text is placed at the start of the canonical name, but the latter will probably be stripped from the sub-part as it duplicates the recording work name; any title text (for "extended" style) will be appended to the whole. Note that, if "Consistent with lowest level work description" is chosen in section 2, the text may be treated as a "prefix" similar to those in the "Advanced" tab. If this eliminates other similar prefixes and has unwanted effects, then either change the desired text slightly (e.g. surround with brackets) or use the "Full MusicBrainz work hierarchy" option in section 2. Note that similar text between the partial work and its 'parent' will be removed which will frequently result in no text other than the specified 'partial text', unless extended metadata is used resulting in appended text in {} - this behaviour can be controlled by disabling the setting "Allow blank part names for arrangements and part recordings..." on the advanced tab.
+
+ * **Arrangements**:
+ If this option is selected, works which are arrangements of other works will have the latter treated in the same manner as "parent" works, except that the arrangement work name will be prefixed by the text provided. Note that similar text between the arranged work and its parent will be removed unless this results in no text, in which case a stricter comparison (as for the derivation of 'part' names from works) will be used.
+
+ **Important note:** *If the Partial or Arrangement options are changed (i.e. selected/deselected) then quit and restart Picard as the work structure is fundamentally different. If the related text (only) is changed then the release can simply be refreshed.*
+
+ * **Medleys**
+ These can occur in two ways in MusicBrainz: (a) the recording is described as a "medley of" a number of works and (b) the track is described as (more than one) "medley including a recording of" a work. See [Homecoming](https://musicbrainz.org/release/393913a2-7fde-4ed5-8be6-ca5c2c0ccf0d) for examples of both (tracks 8, 9 and 11). In the first case, the specified text will be included in brackets after the work name, whereas in the second case, the track will be treated as a recording of multiple works and the specified text will appear in the **parent** work name.
+
+6. "SongKong-compatible tag usage".
+
+ "Use work tags on file (no look up on MB) if Use Cache selected": This will enable the existing work tags on the file to be used in preference to looking up on MusicBrainz, if those tags are SongKong-compatible (which should be the case if SongKong has been used or if the SongKong tags have been previously written by this plugin). If present, this can speed up processing considerably, but obviously any new data on MusicBrainz will be missed. For the option to operate, "Use cache" also needs to be selected. Although faster, many of the subtleties of a full look-up will be missed - for example, parent works which are arrangements will not be highlighted as such, some arrangers or composers of original works may be omitted and some medley information may be missed. Other information, such as composed-dates will also be missing. **In general, therefore, the use of this option will result in poorer metadata than allowing the full database look-up to run. It is not recommended unless you have already tagged your files with SongKong and speed is more important than quality.**
+
+ "Write SongKong-compatible work tags" does what it says. These can then be used by the previous option, if the release is subsequently reloaded into Picard, to speed things up (assuming the reload was not to pick up new work data). The same caveats as those above apply.
+
+ **Note that, as from version 2.0.2, there is no significant speed difference between (basic) SongKong and Picard with Classical Extras, so this feature is only ever useful if the album has already been tagged with SongKong.**
+
+ The default for both these options is unchecked.
+
+ Note that Picard and SongKong use the tag musicbrainz\_workid to mean different things. If Picard has overwritten the SongKong tag (not a problem if this plugin is used) then a warning will be given and the works will be looked up on MusicBrainz. Also note that once a release is loaded, subsequent refreshes will use the cache (if option is ticked) in preference to the file tags.
+
+**Note for iTunes users:** *iTunes and Picard do not work well together. iTunes can display work and movement for m4a(mp4) files, but Picard does not write the movement tag. To work round this, write the movement to the "subtitle" tag assuming that is not otherwise used, and use a simple Mp3tag action to convert it to MOVEMENTNAME before importing to iTunes. If you are writing to a FLAC file which will subsequently be converted to m4a then different tag names may be required; e.g. using dBpoweramp, write the movement to "movement name". In both cases use "work" for the work. To store the top\_work, use "grouping" if writing directly to m4a, but "style" if writing to FLAC followed by dBpoweramp conversion. You can put multiple tags into the boxes described above so that your options are multi-purpose. N.B. if work tags are specified and the work has at least one level (i.e. at least work: movement), then the tag "show work movement" will be set to 1. This is used by iTunes to trigger the hierarchical display and should work both directly with m4a files and indirectly via files which are subsequently converted.*
+
+## Genres etc. tab
+
+This section is dependent on both the artists and workparts sections. If either of those sections are not run then this section will not operate correctly. At the very top of the tab is a checkbox "Use Muso reference database...". For [Muso](http://klarita.net/muso.html) users, selecting this enables you to use reference data for genres, composers and periods which have been entered in Muso's "Options->Classical Music" section. Regardless as to whether this is selected, there are then three main coloured sections, each with a number of subsections. The details in each section differ depending on whether the "Muso" option is selected. The screen print below shows the options assuming it is not selected (differences occurring when "Muso" is selected are discussed later):
+
+![Genres etc.](https://music.highmossergate.co.uk/classical-extras-screenshots/genres-plain/)
+
+1. "Genres". Two separate tags may be used to store genre information, a main genre tage (usually just "genre") and a sub-genre tag. These need to be specified at the top of the section. If either is left blank then the related processing will not run.
+
+ * **Source of genres**
+ Any or all of four sources may be selected. In each case, any values found are treated as "candidate genres" - they will only be applied to the specified genre and sub-genre tags in accordance with the criteria in the "allowed genres" section, if any (see below).
+
+ (a) "Existing file tag". The contents of the existing file tag (as specified above - main genre tag only) will be included as candidate genres. Note that, if this tag name is not "genre", then the contents of the tag "genre" will be included as well.
+
+ (b) "Folksonomy work tags". This will use the folksonomy tags for **works** (including parent works) as a possible source of genres. To use the folksonomy tags for **releases/tracks**, select the main Picard option in Options->Metadata->"Use folksonomy tags as genre". Again (unlike vanilla Picard) these are candidate genres, and will only be published if they match the allowed genres.
+
+ (c) "Work-type". The work-type attribute of works or parent works will be used as a candidate genre.
+
+ (d) "Infer from artist metadata". This option was on the artist tab in version 0.9.1 and prior. Owing to the additional genre processing now available, the operation of this option is slightly restricted compared to the earlier versions. It attempts to create candidate genres based on information in the artist-related tags. Values provided are:
+ Orchestral, Concerto, Choral, Opera, Duet,Trio, Quartet, Chamber music, Aria ('classical values') and Vocal, Song, Instrumental ('generic values'). If the track is a recorded work and the track artist is the composer (i.e. MusicBrainz 'classical style'), the candidate genre values will also include "Classical". The 'classical values' will only be included as candidate genres if the track is deemed to be 'classical' by some part of the genre processing section.
+
+ * **Allowed genres**
+ A check-box (ticked by default) enables this section. If it is unchecked, then no genre filtering is applied - all 'candidate genres' will be written to the genre tab.
+ Four boxes are provided for lists of genres which are "allowed" to appear in the specified tags. Each list should be comma-separated (and no commas in any genre name). Candidate genres matching those in a "main genre" box will be added to the specified main genre tag. Similarly for sub-genres. If a candidate genre matches a 'classical genre' (in one of the top two boxes), then the track will be deemed to be "Classical" (see next part for more details).
+You may also enter a genre name to be used if no matching main genre is found (otherwise the tag will be blank).
+
+ * **"Classical" genre**
+ Normally (i.e. by default) a work will only be deemed to be 'classical' if it is inferred from the MusicBrainz style (see "source of genres") or if a candidate genre matches a "Classical" genre or sub-genre list. However, you may select that all tracks are 'classical' regardless. There is also an option to exclude the word "Classical" from any genre tag, but still treat the work as classical. If a work is deemed to be classical, a tag may be written with a specified value as set out in the last two boxes of this section. For example, to be consistent with SonKong/Jaikoz, you could set "is\_classical" to "1".
+
+2. "Instruments and keys".
+ * **Instruments**
+ Specify the tag name you wish instrument names to appear in. Instruments will be sourced from performer relationships. Instrument names may either be the standard MusicBrainz names or the "credited as" names in the performer relationship, or both. Vocal types are treated similarly to instruments. (Note that, in v0.9.1 and prior, instruments were written to the same tag as inferred genres. If you wish to continue this, then you may use the same tag name here as for the genre tag.)
+
+ * **Keys**
+ Specify the tag name in which you wish the key signatures of works to appear. Keys will be obtained from all work levels (assuming these have been looked up): for example, Dvořák's Largo From the New World will be shown as D♭ major, C# minor (the main keys of the movement) and E minor (the home key of the overall work).
+"Include key(s) in work names" gives the option to include the key signature for a work in brackets after the name of the work in the metadata. Keys will be added in the appropriate levels: e.g. Dvořák's New World Symphony will get (E minor) at the work level, but only movements with different keys will be annotated viz. "II. Largo (D-flat major, C-Sharp minor)". The default sub-option is to only add these details if the key signature is missing from the work title (other sub-options are to never or to always include the information.
+
+3. "Periods and dates".
+
+ * **Work dates**
+ Specify the tag name to hold work dates. Work dates will be given as a "year" value only, e.g. "1808" or a range: "1808-1810". The sources of these dates is specified in the next part. If the movement has a composed date(s), this will be used, otherwise the the dates from the parent work will be used (if available).
+
+ "Source of work dates". Select which sources to use - from composed, published and premiered, then decide whether to use them in preferential order (e.g. if "composed date" exists, then the others will not be used) or to show them all.
+
+ "Include workdate in work name ..." operates analogously to "Include key(s) in work names" described above. (Work dates will be used in preference order, i.e. composed - published - premiered, with only the first available date being shown).
+
+ * **Periods**
+ This section will use work dates, where available, to determine the "classical period" to which it belongs, by means of a "period map" (Muso users can also use composer dates - see below).
+
+ Specify the tag name to hold the period data. The period map should then be entered in the format "Period name, Start\_year, End\_year; Period name2, Start\_year, End\_year;" etc. Periods may overlap. Do not use commas or semi-colons within period names. Start and end years must be integers.
+
+## Genres etc. tab - Muso-specific processing
+
+Users of [Muso](http://klarita.net/muso.html) have additional capabilities, illustrated in the following screen, which appear when the option "Use Muso reference database ..." is selected at the top of the tab.
+
+![Genres etc. - Muso](https://music.highmossergate.co.uk/classical-extras-screenshots/genres-muso/)
+
+For these options to work, the path/name of the Muso reference database needs to be specified on the advanced tab. The default path is "C:\\Users\\Public\\Music\\muso\\database" and the default filename is "Reference.xml". The additional options are as follows.
+
+1. "Use Muso classical genres". If this is selected, the box for classical main genres is eliminated and the genre list from Muso's "Tools->Options->Classical Music->Classical Music Genres" is used instead.
+
+2. "Use Muso composer list to determine if classical". If the composer name is in Muso's list "Tools->Options->Classical Music->Composer Roster", then the work will be deemed to be classical. If this option is selected, a further option appears to "Treat arrangers as for composers" - if selected then arrangers will also be looked up in the roster.
+
+3. "Use Muso composer dates (if no work date) to determine period". The birth date + 20 -> death dates of Muso's composer roster will be used to assign periods if no work date is available. If this option is selected, a further option appears to "Treat arrangers as for composers" - if selected then arrangers' working lives will also be used to determine periods.
+
+ (This might be replaced / supplemented by MusicBrainz in the future, but would involve another 1-second lookup per composer).
+
+4. "Use Muso map". Replace the period map with the one in Muso at "Tools->Options->Classical Music->Classical Music Periods"
+
+Note that non-Muso users may also use this functionality, if they wish, by manually creating a reference xml file with the relevant tags, e.g.:
+
+
+ Artist type Host tag Hidden variable
+ writer composer writers
+ lyricist lyricist lyricists
+ revised by arranger revisors
+ translator lyricist translators
+ arranger arranger arrangers
+ reconstructed by arranger reconstructors
+ orchestrator arranger orchestrators
+ instrument arranger arranger arrangers (with instrument type in brackets)
+ vocal arranger arranger arrangers (with voice type in brackets)
+ chorus master conductor chorusmasters
+ concertmaster performer (with annotation as a sub-key) leaders
+The options screen provides five tabs for users to control the tags produced:
+
+1. Artists: Options as to whether artist tags will contain standard MB names, aliases or as-credited names.
+Ability to include and annotate names for specialist roles (chorus master, arranger, lyricist etc.).
+Ability to read lyrics tags on the file which has been loaded and assign them to track and album levels if required.
+(Note: Picard will not normally process incoming file tags).
+
+2. Works and parts: The plugin will build a hierarchy of works and parts (e.g. Work -> Part -> Movement or
+Opera -> Act -> Number) based on the works in MusicBrainz's database. These can then be displayed in tags in a variety
+of ways according to user preferences. Furthermore partial recordings, medleys, arrangements and collections of works
+are all handled according to user choices. There is a processing overhead for this at present because MusicBrainz limits
+look-ups to one per second.
+
+3. Genres etc.: Options are available to customise the source and display of information relating to genres,
+instruments, keys, work dates and periods. Additional capabilities are provided for users of Muso (or others who
+provide the relevant XML files) to use pre-existing databases of classical genres, classical composers and classical
+periods.
+
+4. Tag mapping: in some ways, this is a simple substitute for some of Picard's scripting capability. The main advantage
+ is that the plugin will remember what tag mapping you use for each release (or even track).
+
+5. Advanced: Various options to control the detailed processing of the above.
+
+All user options can be saved on a per-album (or even per-track) basis so that tweaks can be used to deal with
+inconsistencies in the MusicBrainz data (e.g. include English titles from the track listing where the MusicBrainz works
+are in the composer's language and/or script).
+Also existing file tags can be processed (not possible in native Picard).
+
+See the readme file
+on GitHub here for full details.
+"""
+
+########################
+# DEVELOPERS NOTES: ####
+########################
+# This plugin contains 3 classes:
+#
+# I. ("EXTRA ARTISTS") Create sorted fields for all performers. Creates a number of variables with alternative values
+# for "artists" and "artist".
+# Creates an ensemble variable for all ensemble-type performers.
+# Also creates matching sort fields for artist and artists.
+# Additionally create tags for artist types which are not normally created in Picard - particularly for classical music
+# (notably instrument arrangers).
+#
+# II. ("PART LEVELS" [aka Work Parts]) Create tags for the hierarchy of works which contain a given track recording
+# - particularly for classical music'
+# Variables provided for each work level, with implied part names
+# Mixed metadata provided including work and title elements
+#
+# III. ("OPTIONS") Allows the user to set various options including what tags will be written
+# (otherwise the classes above will just write outputs to "hidden variables")
+#
+# The main control routine is at the end of the module
+
+PLUGIN_VERSION = '2.0.7'
+PLUGIN_API_VERSIONS = ["2.0", "2.1", "2.2"]
+PLUGIN_LICENSE = "GPL-2.0"
+PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"
+
+from picard.ui.options import register_options_page, OptionsPage
+from picard.plugins.classical_extras.ui_options_classical_extras import Ui_ClassicalExtrasOptionsPage
+import picard.plugins.classical_extras.suffixtree
+from picard import config, log
+from picard.config import ConfigSection, BoolOption, IntOption, TextOption
+from picard.util import LockableObject, uniqify
+
+# note that in 2.0 picard.webservice changed to picard.util.xml
+from picard.util.xml import XmlNode
+from picard.util import translate_from_sortname
+from picard.metadata import register_track_metadata_processor, Metadata
+from functools import partial
+from datetime import datetime
+import collections
+import re
+import unicodedata
+import json
+import copy
+import os
+from PyQt5.QtCore import QXmlStreamReader
+from picard.const import USER_DIR
+import operator
+import ast
+import picard.plugins.classical_extras.const
+
+
+
+##########################
+# MODULE-WIDE COMPONENTS #
+##########################
+# CONSTANTS
+# N.B. Constants with long definitions are set in const.py
+PRESERVE = [x.strip() for x in config.setting["preserved_tags"].split(',')]
+DATE_SEP = '-'
+
+# COMMONLY USED REGEX
+ROMAN_NUMERALS = r'\b((?=[MDCLXVI])(M{0,4}(CM|CD|D?)?C{0,3}(XC|XL|L?)?X{0,3}(IX|IV|V?)?I{0,3}))(?:\.|\-|:|;|,|\s|$)'
+ROMAN_NUMERALS_AT_START = r'^\W*' + ROMAN_NUMERALS
+RE_ROMANS = re.compile(ROMAN_NUMERALS, re.IGNORECASE)
+RE_ROMANS_AT_START = re.compile(ROMAN_NUMERALS_AT_START, re.IGNORECASE)
+# KEYS
+RE_NOTES = r'(\b[ABCDEFG])'
+RE_ACCENTS = r'(\-sharp(?:\s+|\b)|\-flat(?:\s+|\b)|\ssharp(?:\s+|\b)|\sflat(?:\s+|\b)|\u266F(?:\s+|\b)|\u266D(?:\s+|\b)|(?:[:,.]?\s+|$|\-))'
+RE_SCALES = r'(major|minor)?(?:\b|$)'
+RE_KEYS = re.compile(
+ RE_NOTES + RE_ACCENTS + RE_SCALES,
+ re.UNICODE | re.IGNORECASE)
+
+# LOGGING
+
+# If logging occurs before any album is loaded, the startup log file will
+# be written
+log_files = collections.defaultdict(dict)
+# entries are release-ids: to keep track of which log files are open
+release_status = collections.defaultdict(dict)
+# release_status[release_id]['works'] = True indicates that we are still processing works for release_id
+# & similarly for 'artists'
+# release_status[release_id]['start'] holds start time of release processing
+# release_status[release_id]['name'] holds the album name
+# release_status[release_id]['lookups'] holds number of lookups for this release
+# release_status[release_id]['file_objects'] holds a cumulative list of file objects (tagger seems a bit unreliable)
+# release_status[release_id]['file_found'] = False indicates that "No file
+# with matching trackid" has (yet) been found
+
+
+def write_log(release_id, log_type, message, *args):
+ """
+ Custom logging function - if log_info is set, all messages will be written to a custom file in a 'Classical_Extras'
+ subdirectory in the same directory as the main Picard log. A different file is used for each album,
+ to aid in debugging - the log file is release_id.log. Any startup messages (i.e. before a release has been loaded)
+ are written to session.log. Summary information for each release is also written to session.log even if log_info
+ is not set.
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param log_type: 'error', 'warning', 'debug' or 'info'
+ :param message: string, e.g. 'error message for workid: %s'
+ :param args: arguments for parameters in string, e.g. if workId then str(workId) will replace %s in the above
+ :return:
+ """
+ options = config.setting
+ if not isinstance(message, str):
+ msg = repr(message)
+ else:
+ msg = message
+ if args:
+ msg = msg % args
+
+ if options["log_info"] or log_type == "basic":
+ # if log_info is True, all log messages will be written to the custom log, regardless of other log_... settings
+ # basic session log will always be written (summary of releases and
+ # processing times)
+ filename = release_id + ".log"
+ log_dir = os.path.join(USER_DIR, "Classical_Extras")
+ if not os.path.exists(log_dir):
+ os.makedirs(log_dir)
+ if release_id not in log_files:
+ try:
+ if release_id == 'session':
+ log_file = open(
+ os.path.join(
+ log_dir,
+ filename),
+ 'w',
+ encoding='utf8',
+ buffering=1)
+ # buffering=1 so that session log (low volume) is up to
+ # date even if not closed
+ else:
+ log_file = open(
+ os.path.join(
+ log_dir,
+ filename),
+ 'w',
+ encoding='utf8') # , buffering=1)
+ # default buffering for speed, buffering = 1 for currency
+ log_files[release_id] = log_file
+ log_file.write(
+ PLUGIN_NAME +
+ ' Version:' +
+ PLUGIN_VERSION +
+ '\n')
+ if release_id == 'session':
+ log_file.write('session' + '\n')
+ else:
+ log_file.write('Release id: ' + release_id + '\n')
+ if release_id in release_status and 'name' in release_status[release_id]:
+ log_file.write(
+ 'Album name: ' + release_status[release_id]['name'] + '\n')
+ except IOError:
+ log.error('Unable to open file %s for writing log', filename)
+ return
+ else:
+ log_file = log_files[release_id]
+ try:
+ log_file.write(log_type[0].upper() + ': ')
+ log_file.write(str(datetime.now()) + ' : ')
+ log_file.write(msg)
+ log_file.write("\n")
+ except IOError:
+ log.error('Unable to write to log file %s', filename)
+ return
+ # Only debug, warning and error messages will be written to the main
+ # Picard log, if those options have been set
+ if log_type != 'info' and log_type != 'basic': # i.e. non-custom log items
+ message2 = PLUGIN_NAME + ': ' + message
+ else:
+ message2 = message
+ if log_type == 'debug' and options["log_debug"]:
+ if release_id in release_status and 'debug' in release_status[release_id]:
+ add_list_uniquely(release_status[release_id]['debug'], msg)
+ else:
+ release_status[release_id]['debug'] = [msg]
+ log.debug(message2, *args)
+ if log_type == 'warning' and options["log_warning"]:
+ if release_id in release_status and 'warnings' in release_status[release_id]:
+ add_list_uniquely(release_status[release_id]['warnings'], msg)
+ else:
+ release_status[release_id]['warnings'] = [msg]
+ if args:
+ log.warning(message2, *args)
+ else:
+ log.warning(message2)
+ if log_type == 'error' and options["log_error"]:
+ if release_id in release_status and 'errors' in release_status[release_id]:
+ add_list_uniquely(release_status[release_id]['errors'], msg)
+ else:
+ release_status[release_id]['errors'] = [msg]
+ if args:
+ log.error(message2, *args)
+ else:
+ log.error(message2)
+
+
+def close_log(release_id, caller):
+ # close the custom log file if we are done
+ if release_id == 'session': # shouldn't happen but, just in case, don't close the session log
+ return
+ if caller in ['works', 'artists']:
+ release_status[release_id][caller] = False
+ if (caller == 'works' and release_status[release_id]['artists']) or \
+ (caller == 'artists' and release_status[release_id]['works']):
+ # log.error('exiting close_log. only %s done', caller) # debug line
+ return
+ duration = 'N/A'
+ lookups = 'N/A'
+ artists_time = 0
+ works_time = 0
+ lookup_time = 0
+ album_process_time = 0
+ if release_id in release_status:
+ duration = datetime.now() - release_status[release_id]['start']
+ lookups = release_status[release_id]['lookups']
+ done_lookups = release_status[release_id]['done-lookups']
+ lookup_time = done_lookups - release_status[release_id]['start']
+ album_process_time = duration - lookup_time
+ artists_time = release_status[release_id]['artists-done'] - \
+ release_status[release_id]['start']
+ works_time = release_status[release_id]['works-done'] - \
+ release_status[release_id]['start']
+ del release_status[release_id]['start']
+ del release_status[release_id]['lookups']
+ del release_status[release_id]['done-lookups']
+ del release_status[release_id]['artists-done']
+ del release_status[release_id]['works-done']
+ if release_id in log_files:
+ write_log(
+ release_id,
+ 'info',
+ 'Duration = %s. Number of lookups = %s.',
+ duration,
+ lookups)
+ write_log(release_id, 'info', 'Closing log file for %s', release_id)
+ log_files[release_id].close()
+ del log_files[release_id]
+ if 'session' in log_files and release_id in release_status:
+ write_log(
+ 'session',
+ 'basic',
+ '\n Completed processing release id %s. Details below:-',
+ release_id)
+ if 'name' in release_status[release_id]:
+ write_log('session', 'basic', 'Album name %s',
+ release_status[release_id]['name'])
+ if 'errors' in release_status[release_id]:
+ write_log(
+ 'session',
+ 'basic',
+ '-------------------- Errors --------------------')
+ for error in release_status[release_id]['errors']:
+ write_log('session', 'basic', error)
+ del release_status[release_id]['errors']
+ if 'warnings' in release_status[release_id]:
+ write_log(
+ 'session',
+ 'basic',
+ '-------------------- Warnings --------------------')
+ for warning in release_status[release_id]['warnings']:
+ write_log('session', 'basic', warning)
+ del release_status[release_id]['warnings']
+ if 'debug' in release_status[release_id]:
+ write_log(
+ 'session',
+ 'basic',
+ '-------------------- Debug log --------------------')
+ for debug in release_status[release_id]['debug']:
+ write_log('session', 'basic', debug)
+ del release_status[release_id]['debug']
+ write_log(
+ 'session',
+ 'basic',
+ 'Duration = %s. Artists time = %s. Works time = %s. Of which: Lookup time = %s. '
+ 'Album-process time = %s. Number of lookups = %s.',
+ duration,
+ artists_time,
+ works_time,
+ lookup_time,
+ album_process_time,
+ lookups)
+ if release_id in release_status:
+ del release_status[release_id]
+
+
+# FILE READING AND OBJECT PARSING
+
+_node_name_re = re.compile('[^a-zA-Z0-9]')
+
+
+def _node_name(n):
+ return _node_name_re.sub('_', str(n))
+
+
+def _read_xml(stream):
+ document = XmlNode()
+ current_node = document
+ path = []
+ while not stream.atEnd():
+ stream.readNext()
+ if stream.isStartElement():
+ node = XmlNode()
+ attrs = stream.attributes()
+ for i in range(attrs.count()):
+ attr = attrs.at(i)
+ node.attribs[_node_name(attr.name())] = str(attr.value())
+ current_node.append_child(_node_name(stream.name()), node)
+ path.append(current_node)
+ current_node = node
+ elif stream.isEndElement():
+ current_node = path.pop()
+ elif stream.isCharacters():
+ current_node.text += str(stream.text())
+ return document
+
+
+def parse_data(release_id, obj, response_list, *match):
+ """
+ This function takes any XmlNode object, or list thereof, or a JSON object
+ and extracts a list of all objects exactly matching the hierarchy listed in match.
+ match should contain list of each node in hierarchical sequence, with no gaps in the sequence
+ of nodes, to lowest level required.
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param obj: an XmlNode or JSON object, list or dictionary containing nodes
+ :param response_list: working memory for recursive calls
+ :param match: list of items to search for in node (see detailed notes below)
+ :return: a list of matching items (always a list, even if only one item)
+
+ Insert attribs.attribname:attribvalue in the list to select only branches where attribname
+ is attribvalue. (Omit the attribs prefix if the obj is JSON)
+ Insert childname.text:childtext in the list to select only branches where
+ a sibling with childname has text childtext.
+ (Note: childname can be a dot-list if the text is more than one level down - e.g. child1.child2
+ # TODO - Check this works fully )
+ """
+ if '!log' in response_list:
+ DEBUG = True
+ INFO = True
+ else:
+ DEBUG = False
+ INFO = False
+ # Normally logging options are off as these can be VERY wordy
+ # They can be turned on by using !log in the call
+
+ # XmlNode instances are not iterable, so need to convert to dict
+ if isinstance(obj, XmlNode):
+ obj = obj.__dict__
+ if DEBUG or INFO:
+ write_log(release_id, 'debug', 'Parsing data - looking for %s', match)
+ if INFO:
+ write_log(release_id, 'info', 'Looking in object: %s', obj)
+ if isinstance(obj, list):
+ objlen = len(obj)
+ for i, item in enumerate(obj):
+ if isinstance(item, XmlNode):
+ item = item.__dict__
+ if INFO:
+ write_log(
+ release_id,
+ 'info',
+ 'Getting response for list item no.%s of %s - object is: %s',
+ i + 1,
+ objlen,
+ item)
+ parse_data(release_id, item, response_list, *match)
+ if INFO:
+ write_log(
+ release_id,
+ 'info',
+ 'response_list for list item no.%s of %s is %s',
+ i + 1,
+ objlen,
+ response_list)
+ return response_list
+ elif isinstance(obj, dict):
+ if match[0] in obj:
+ if len(match) == 1:
+ response = obj[match[0]]
+ if response is not None: # To prevent adding NoneTypes to list
+ response_list.append(response)
+ if INFO:
+ write_log(
+ release_id,
+ 'info',
+ 'response_list (last match item): %s',
+ response_list)
+ else:
+ match_list = list(match)
+ match_list.pop(0)
+ parse_data(release_id, obj[match[0]],
+ response_list, *match_list)
+ if INFO:
+ write_log(
+ release_id,
+ 'info',
+ 'response_list (passing up): %s',
+ response_list)
+ return response_list
+ elif ':' in match[0]:
+ test = match[0].split(':')
+ match2 = test[0].split('.')
+ test_data = parse_data(release_id, obj, [], *match2)
+ if INFO:
+ write_log(
+ release_id,
+ 'info',
+ 'Value comparison - looking in %s for value %s',
+ test_data,
+ test[1])
+ if len(test) > 1:
+ # latter is because Booleans are stored as such, not as
+ # strings, in JSON
+ if (test[1] in test_data) or (
+ (test[1] == 'True') in test_data):
+ if len(match) == 1:
+ response = obj
+ if response is not None:
+ response_list.append(response)
+ else:
+ match_list = list(match)
+ match_list.pop(0)
+ parse_data(release_id, obj, response_list, *match_list)
+ else:
+ parse_data(release_id, obj, response_list, *match2)
+ if INFO:
+ write_log(
+ release_id,
+ 'info',
+ 'response_list (from value look-up): %s',
+ response_list)
+ return response_list
+ else:
+ if 'children' in obj:
+ parse_data(release_id, obj['children'], response_list, *match)
+ if INFO:
+ write_log(
+ release_id,
+ 'info',
+ 'response_list (from children): %s',
+ response_list)
+ return response_list
+ else:
+ if INFO:
+ write_log(
+ release_id,
+ 'info',
+ 'response_list (obj is not a list or dict): %s',
+ response_list)
+ return response_list
+
+
+def create_dict_from_ref_list(options, release_id, ref_list, keys, tags):
+ ref_dict_list = []
+ for refs in ref_list:
+ for ref in refs:
+ parsed_refs = [
+ parse_data(
+ release_id,
+ ref,
+ [],
+ t,
+ 'text') for t in tags]
+ ref_dict_list.append(dict(zip(keys, parsed_refs)))
+ return ref_dict_list
+
+
+def get_references_from_file(release_id, path, filename):
+ """
+ Lookup Muso Reference.xml or similar
+ :param release_id: name of log file
+ :param path: Reference file path
+ :param filename: Reference file name
+ :return:
+ """
+ options = config.setting
+ composer_dict_list = []
+ period_dict_list = []
+ genre_dict_list = []
+ xml_file = None
+ try:
+ xml_file = open(os.path.join(path, filename), encoding="utf8")
+ reply = xml_file.read()
+ xml_file.close()
+ document = _read_xml(QXmlStreamReader(reply))
+ # Composers
+ composer_list = parse_data(
+ release_id, document, [], 'ReferenceDB', 'Composer')
+ keys = ['name', 'sort', 'birth', 'death', 'country', 'core']
+ tags = ['Name', 'Sort', 'Birth', 'Death', 'CountryCode', 'Core']
+ composer_dict_list = create_dict_from_ref_list(
+ options, release_id, composer_list, keys, tags)
+ # Periods
+ period_list = parse_data(
+ release_id,
+ document,
+ [],
+ 'ReferenceDB',
+ 'ClassicalPeriod')
+ keys = ['name', 'start', 'end']
+ tags = ['Name', 'Start_x0020_Date', 'End_x0020_Date']
+ period_dict_list = create_dict_from_ref_list(
+ options, release_id, period_list, keys, tags)
+ # Genres
+ genre_list = parse_data(
+ release_id,
+ document,
+ [],
+ 'ReferenceDB',
+ 'ClassicalGenre')
+ keys = ['name']
+ tags = ['Name']
+ genre_dict_list = create_dict_from_ref_list(
+ options, release_id, genre_list, keys, tags)
+
+ except (IOError, FileNotFoundError, UnicodeDecodeError):
+ if options['cwp_muso_genres'] or options['cwp_muso_classical'] or options['cwp_muso_dates'] or options['cwp_muso_periods']:
+ write_log(
+ release_id,
+ 'error',
+ 'File %s does not exist or is corrupted',
+ os.path.join(
+ path,
+ filename))
+ finally:
+ if xml_file:
+ xml_file.close()
+ return {
+ 'composers': composer_dict_list,
+ 'periods': period_dict_list,
+ 'genres': genre_dict_list}
+
+# OPTIONS
+
+
+def get_options(release_id, album, track):
+ """
+ Get the saved options from a release and use them according to flags set on the "advanced" tab
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param album: current release
+ :param track: current track
+ :return: None (result is passed via tm)
+ A common function for both Artist and Workparts, so that the first class to process a track will execute
+ this function so that the results are available to both (via a track metadata item)
+ """
+ release_status[release_id]['done'] = False
+ set_options = collections.defaultdict(dict)
+ main_sections = ['artists', 'workparts']
+ all_sections = ['artists', 'tag', 'workparts', 'genres']
+ parent_sections = {
+ 'artists': 'artists',
+ 'tag': 'artists',
+ 'workparts': 'workparts',
+ 'genres': 'workparts'}
+ # The above needs to be done for legacy reasons - there are only two tags which store options - artists and workparts
+ # This dates from when there were only two sections
+ # To split these now will create compatibility issues
+ override = {
+ 'artists': 'cea_override',
+ 'tag': 'ce_tagmap_override',
+ 'workparts': 'cwp_override',
+ 'genres': 'ce_genres_override'}
+ sect_text = {'artists': 'Artists', 'workparts': 'Works'}
+ prefix = {'artists': 'cea', 'workparts': 'cwp'}
+
+ if album.tagger.config.setting['ce_options_overwrite'] and all(
+ album.tagger.config.setting[override[sect]] for sect in main_sections):
+ set_options[track] = album.tagger.config.setting # mutable
+ else:
+ set_options[track] = option_settings(
+ album.tagger.config.setting) # make a copy
+ if set_options[track]["log_info"]:
+ write_log(
+ release_id,
+ 'info',
+ 'Default (i.e. per UI) options for track %s are %r',
+ track,
+ set_options[track])
+
+ # As we use some of the main Picard options and may over-write them, save them here
+ # set_options[track]['translate_artist_names'] = config.setting['translate_artist_names']
+ # set_options[track]['standardize_artists'] = config.setting['standardize_artists']
+ # (not sure this is needed - TODO reconsider)
+
+ options = set_options[track]
+ tm = track.metadata
+ new_metadata = None
+ orig_metadata = None
+ # Only look up files if needed
+ file_options = {}
+ music_file = ''
+ music_file_found = None
+ release_status[release_id]['file_found'] = False
+ start = datetime.now()
+ if options["log_info"]:
+ write_log(release_id, 'info', 'Clock start at %s', start)
+ trackno = tm['tracknumber']
+ discno = tm['discnumber']
+
+ album_filenames = album.tagger.get_files_from_objects([album])
+ if options["log_info"]:
+ write_log(
+ release_id,
+ 'info',
+ 'No. of album files found = %s',
+ len(album_filenames))
+ # Note that sometimes Picard fails to get all the file objects, even if they are there (network issues)
+ # so we will cache whatever we can get!
+ if release_id in release_status and 'file_objects' in release_status[release_id]:
+ add_list_uniquely(
+ release_status[release_id]['file_objects'],
+ album_filenames)
+ else:
+ release_status[release_id]['file_objects'] = album_filenames
+ if options["log_info"]:
+ write_log(release_id, 'info', 'No. of album files cached = %s',
+ len(release_status[release_id]['file_objects']))
+ track_file = None
+ for album_file in release_status[release_id]['file_objects']:
+ if options["log_info"]:
+ write_log(release_id,
+ 'info',
+ 'Track file = %s, tracknumber = %s, discnumber = %s. Metadata trackno = %s, discno = %s',
+ album_file.filename,
+ str(album_file.tracknumber),
+ str(album_file.discnumber),
+ trackno,
+ discno)
+ if str(
+ album_file.tracknumber) == trackno and str(
+ album_file.discnumber) == discno:
+ if options["log_info"]:
+ write_log(
+ release_id,
+ 'info',
+ 'Track file found = %r',
+ album_file.filename)
+ track_file = album_file.filename
+ break
+
+ # Note: It would have been nice to do a rough check beforehand of total tracks,
+ # but ~totalalbumtracks is not yet populated
+ if not track_file:
+ album_fullnames = [
+ x.filename for x in release_status[release_id]['file_objects']]
+ if options["log_info"]:
+ write_log(
+ release_id,
+ 'info',
+ 'Album files found = %r',
+ album_fullnames)
+ for music_file in album_fullnames:
+ new_metadata = album.tagger.files[music_file].metadata
+
+ if 'musicbrainz_trackid' in new_metadata and 'musicbrainz_trackid' in tm:
+ if new_metadata['musicbrainz_trackid'] == tm['musicbrainz_trackid']:
+ track_file = music_file
+ break
+ # Nothing found...
+ if new_metadata and 'musicbrainz_trackid' not in new_metadata:
+ if options['log_warning']:
+ write_log(
+ release_id,
+ 'warning',
+ 'No trackid in file %s',
+ music_file)
+ if 'musicbrainz_trackid' not in tm:
+ if options['log_warning']:
+ write_log(
+ release_id,
+ 'warning',
+ 'No trackid in track %s',
+ track)
+ #
+ # Note that, on initial load, new_metadata == orig_metadata; but, after refresh, new_metadata will have
+ # the same track metadata as tm (plus the file metadata as per orig_metadata), so a trackid match
+ # is then possible for files that do not have musicbrainz_trackid in orig_metadata. That is why
+ # new_metadata is used in the above test, rather than orig_metadata, but orig_metadata is then used below
+ # to get the saved options.
+ #
+
+ # Find the tag with the options:-
+ if track_file:
+ orig_metadata = album.tagger.files[track_file].orig_metadata
+ music_file_found = track_file
+ if options['log_info']:
+ write_log(
+ release_id,
+ 'info',
+ 'orig_metadata for file %s is',
+ music_file)
+ write_log(release_id, 'info', orig_metadata)
+ for child_section in all_sections:
+ section = parent_sections[child_section]
+ if options[override[child_section]]:
+ if options[prefix[section] + '_options_tag'] + ':' + \
+ section + '_options' in orig_metadata:
+ file_options[section] = interpret(
+ orig_metadata[options[prefix[section] + '_options_tag'] + ':' + section + '_options'])
+ elif options[prefix[section] + '_options_tag'] in orig_metadata:
+ options_tag_contents = orig_metadata[options[prefix[section] + '_options_tag']]
+ if isinstance(options_tag_contents, list):
+ options_tag_contents = options_tag_contents[0]
+ combined_options = ''.join(options_tag_contents.split(
+ '(workparts_options)')).split('(artists_options)')
+ for i, _ in enumerate(combined_options):
+ combined_options[i] = interpret(
+ combined_options[i].lstrip('; '))
+ if isinstance(
+ combined_options[i],
+ dict) and 'Classical Extras' in combined_options[i]:
+ if sect_text[section] + \
+ ' options' in combined_options[i]['Classical Extras']:
+ file_options[section] = combined_options[i]
+ else:
+ for om in orig_metadata:
+ if ':' + section + '_options' in om:
+ file_options[section] = interpret(
+ orig_metadata[om])
+ if section not in file_options or not file_options[section]:
+ if options['log_error']:
+ write_log(
+ release_id,
+ 'error',
+ 'Saved ' +
+ section +
+ ' options cannot be read for file %s. Using current settings',
+ music_file)
+ append_tag(
+ release_id,
+ tm,
+ '~' +
+ prefix[section] +
+ '_error',
+ '1. Saved ' +
+ section +
+ ' options cannot be read. Using current settings')
+
+ release_status[release_id]['file_found'] = True
+
+ end = datetime.now()
+ if options['log_info']:
+ write_log(release_id, 'info', 'Clock end at %s', end)
+ write_log(release_id, 'info', 'Duration = %s', end - start)
+
+ if not release_status[release_id]['file_found']:
+ if options['log_warning']:
+ write_log(
+ release_id,
+ 'warning',
+ "No file with matching trackid for track %s. IF THERE SHOULD BE ONE, TRY 'REFRESH'",
+ track)
+ append_tag(
+ release_id,
+ tm,
+ "002_important_warning",
+ "No file with matching trackid - IF THERE SHOULD BE ONE, TRY 'REFRESH' - "
+ "(unable to process any saved options, lyrics or 'keep' tags)")
+ # Nothing else is done with this info as yet - ideally we need to refresh and re-run
+ # for all releases where, say, release_status[release_id]['file_prob']
+ # == True TODO?
+
+ else:
+ if options['log_info']:
+ write_log(
+ release_id,
+ 'info',
+ 'Found music file: %r',
+ music_file_found)
+ for section in all_sections:
+ if options[override[section]]:
+ parent_section = parent_sections[section]
+ if parent_section in file_options and file_options[parent_section]:
+ try:
+ options_dict = file_options[parent_section]['Classical Extras'][sect_text[parent_section] + ' options']
+ except TypeError as err:
+ if options['log_error']:
+ write_log(
+ release_id,
+ 'error',
+ 'Error: %s. Saved ' +
+ section +
+ ' options cannot be read for file %s. Using current settings',
+ err,
+ music_file)
+ append_tag(
+ release_id,
+ tm,
+ '~' +
+ prefix[parent_section] +
+ '_error',
+ '1. Saved ' +
+ parent_section +
+ ' options cannot be read. Using current settings')
+ break
+ for opt in options_dict:
+ if isinstance(
+ options_dict[opt],
+ dict) and options[override['tag']]: # for tag line options
+ # **NB tag mapping lines are the only entries of type dict**
+ opt_list = []
+ for opt_item in options_dict[opt]:
+ opt_list.append(
+ {opt + '_' + opt_item: options_dict[opt][opt_item]})
+ else:
+ opt_list = [{opt: options_dict[opt]}]
+ for opt_dict in opt_list:
+ for opt_det in opt_dict:
+ opt_value = opt_dict[opt_det]
+ addn = []
+ if section == 'artists':
+ addn = plugin_options('picard')
+ if section == 'tag':
+ addn = plugin_options('tag_detail')
+ for ea_opt in plugin_options(section) + addn:
+ displayed_option = options[ea_opt['option']]
+ if ea_opt['name'] == opt_det:
+ if 'value' in ea_opt:
+ if ea_opt['value'] == opt_value:
+ options[ea_opt['option']] = True
+ else:
+ options[ea_opt['option']
+ ] = False
+ else:
+ options[ea_opt['option']
+ ] = opt_value
+ if options[ea_opt['option']
+ ] != displayed_option:
+ if options['log_debug'] or options['log_info']:
+ write_log(
+ release_id,
+ 'info',
+ 'Options overridden for option %s = %s',
+ ea_opt['option'],
+ opt_value)
+
+ opt_text = str(opt_value)
+ append_tag(
+ release_id, tm, '003_information:options_overridden', str(
+ ea_opt['name']) + ' = ' + opt_text)
+
+ if orig_metadata:
+ keep_list = options['cea_keep'].split(",")
+ if options['cea_split_lyrics'] and options['cea_lyrics_tag']:
+ keep_list.append(options['cea_lyrics_tag'])
+ if options['cwp_genres_use_file']:
+ if 'genre' in orig_metadata:
+ append_tag(
+ release_id,
+ tm,
+ '~cwp_candidate_genres',
+ orig_metadata['genre'])
+ if options['cwp_genre_tag'] and options['cwp_genre_tag'] in orig_metadata:
+ keep_list.append(options['cwp_genre_tag'])
+ really_keep_list = PRESERVE[:]
+ really_keep_list.append(
+ options['cwp_options_tag'] +
+ ':workparts_options')
+ really_keep_list.append(
+ options['cea_options_tag'] +
+ ':artists_options')
+ for tagx in keep_list:
+ tag = tagx.strip()
+ really_keep_list.append(tag)
+ if tag in orig_metadata:
+ append_tag(release_id, tm, tag, orig_metadata[tag])
+ if options['cea_clear_tags']:
+ delete_list = []
+ for tag_item in orig_metadata:
+ if tag_item not in really_keep_list and tag_item[0] != '~':
+ # the second condition is to ensure that (hidden) file variables are not deleted,
+ # as these are in orig_metadata, not track_metadata
+ delete_list.append(tag_item)
+ # this will be used in map_tags to delete unwanted tags
+ options['delete_tags'] = delete_list
+ ## Create a "mirror" tag with the old data, for comparison purposes
+ mirror_tags = []
+ for tag_item in orig_metadata:
+ mirror_name = tag_item + '_OLD'
+ if mirror_name[0] == '~' :
+ mirror_name.replace('~', '_')
+ mirror_name = '~' + mirror_name
+ mirror_tags.append((mirror_name, tag_item))
+ append_tag(release_id, tm, mirror_name, orig_metadata[tag_item])
+ append_tag(release_id, tm, '~ce_mirror_tags', mirror_tags)
+
+ if not isinstance(options, dict):
+ options_dict = option_settings(config.setting)
+ write_log(
+ 'session',
+ 'info',
+ 'Using option_settings(config.setting): %s',
+ options_dict)
+ else:
+ options_dict = options
+ write_log(
+ 'session',
+ 'info',
+ 'Using options: %s',
+ options_dict)
+ tm['~ce_options'] = str(options_dict)
+ tm['~ce_file'] = music_file_found
+
+
+def plugin_options(option_type):
+ """
+ :param option_type: artists, tag, workparts, genres or other
+ :return: the relevant dictionary for the type
+ This function contains all the options data in one place - to prevent multiple repetitions elsewhere
+ """
+ if option_type == 'artists':
+ return const.ARTISTS_OPTIONS
+ elif option_type == 'tag':
+ return const.TAG_OPTIONS
+ elif option_type == 'tag_detail':
+ return const.TAG_DETAIL_OPTIONS
+ elif option_type == 'workparts':
+ return const.WORKPARTS_OPTIONS
+ elif option_type == 'genres':
+ return const.GENRE_OPTIONS
+ elif option_type == 'picard':
+ return const.PICARD_OPTIONS
+ elif option_type == 'other':
+ return const.OTHER_OPTIONS
+ else:
+ return None
+
+def option_settings(config_settings):
+ """
+ :param config_settings: options from UI
+ :return: a (deep) copy of the Classical Extras options
+ """
+ options = {}
+ for option in plugin_options('artists') + plugin_options('tag') + plugin_options('tag_detail') + plugin_options(
+ 'workparts') + plugin_options('genres') + plugin_options('picard') + plugin_options('other'):
+ options[option['option']] = copy.deepcopy(
+ config_settings[option['option']])
+ return options
+
+
+def get_aliases(self, release_id, album, options, releaseXmlNode):
+ """
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param self:
+ :param album:
+ :param options:
+ :param releaseXmlNode: all the metadata for the release
+ :return: Data is returned via self.artist_aliases and self.artist_credits[album]
+
+ Note regarding aliases and credited-as names:
+ In a MB release, an artist can appear in one of seven contexts. Each of these is accessible in releaseXmlNode
+ and the track and recording contexts are also accessible in trackXmlNode.
+ The seven contexts are:
+ Recording: credited-as and alias
+ Release-group: credited-as and alias
+ Release: credited-as and alias
+ Release relationship: credited-as and (not reliably?) alias
+ Recording relationship (direct): credited-as and (not reliably?) alias
+ Recording relationship (via work): credited-as and (not reliably?) alias
+ Track: credited-as and alias
+ (The above are applied in sequence - e.g. track artist credit will over-ride release artist credit. "Recording" gets
+ the lowest priority as it is more generic than the release data {may apply to multiple releases})
+ This function collects all the available aliases and as-credited names once (on processing the first track).
+ N.B. if more than one release is loaded in Picard, any available alias names loaded so far will be available
+ and used. However, as-credited names will only be used from the current release."""
+
+ if 'artist_locale' in config.setting and options['cea_aliases'] or options['cea_aliases_composer']:
+ locale = config.setting["artist_locale"]
+ lang = locale.split("_")[0] # NB this is the Picard code in /util
+
+ # Track and recording aliases/credits are gathered by parsing the
+ # media, track and recording nodes
+ # Do the recording relationship first as it may apply to multiple releases, so release and track data
+ # is more specific.
+ media = parse_data(release_id, releaseXmlNode, [], 'media')
+ for m in media:
+ # disc_num = int(parse_data(options, m, [], 'position', 'text')[0])
+ # not currently used
+ tracks = parse_data(release_id, m, [], 'tracks')
+ for track in tracks:
+ for t in track:
+ # track_num = int(parse_data(options, t, [], 'number',
+ # 'text')[0]) # not currently used
+
+ # Recording artists
+ obj = parse_data(release_id, t, [], 'recording')
+ get_aliases_and_credits(
+ self,
+ options,
+ release_id,
+ album,
+ obj,
+ lang,
+ options['cea_recording_credited'])
+
+ # Get the release data before the recording relationshiops and track data
+ # Release group artists
+ obj = parse_data(release_id, releaseXmlNode, [], 'release-group')
+ get_aliases_and_credits(
+ self,
+ options,
+ release_id,
+ album,
+ obj,
+ lang,
+ options['cea_group_credited'])
+
+ # Release artists
+ get_aliases_and_credits(
+ self,
+ options,
+ release_id,
+ album,
+ releaseXmlNode,
+ lang,
+ options['cea_credited'])
+ # Next bit needed to identify artists who are album artists
+ self.release_artists_sort[album] = parse_data(
+ release_id, releaseXmlNode, [], 'artist-credit', 'artist', 'sort-name')
+ # Release relationship artists
+ get_relation_credits(
+ self,
+ options,
+ release_id,
+ album,
+ releaseXmlNode,
+ lang,
+ options['cea_release_relationship_credited'])
+
+ # Now get the rest:
+ for m in media:
+ tracks = parse_data(release_id, m, [], 'tracks')
+ for track in tracks:
+ for t in track:
+ # Recording relationship artists
+ obj = parse_data(release_id, t, [], 'recording')
+ get_relation_credits(
+ self,
+ options,
+ release_id,
+ album,
+ obj,
+ lang,
+ options['cea_recording_relationship_credited'])
+ # Track artists
+ get_aliases_and_credits(
+ self,
+ options,
+ release_id,
+ album,
+ t,
+ lang,
+ options['cea_track_credited'])
+
+ if options['log_info']:
+ write_log(release_id, 'info', 'Alias and credits info for %s', self)
+ write_log(release_id, 'info', 'Aliases :%s', self.artist_aliases)
+ write_log(
+ release_id,
+ 'info',
+ 'Credits :%s',
+ self.artist_credits[album])
+
+
+def get_artists(options, release_id, tm, relations, relation_type):
+ """
+ Get artist info from XML lookup
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param options:
+ :param tm:
+ :param relations:
+ :param relation_type: 'release', 'recording' or 'work' (NB 'work' does not pass a param for tm)
+ :return:
+ """
+ if options['log_debug'] or options['log_info']:
+ write_log(
+ release_id,
+ 'debug',
+ 'In get_artists. relation_type: %s, relations: %s',
+ relation_type,
+ relations)
+ log_options = {
+ 'log_debug': options['log_debug'],
+ 'log_info': options['log_info']}
+ artists = []
+ instruments = []
+ artist_types = const.RELATION_TYPES[relation_type]
+ for artist_type in artist_types:
+ artists, instruments = create_artist_data(release_id, options, log_options, tm, relations,
+ relation_type, artist_type, artists, instruments)
+ artist_dict = {'artists': artists, 'instruments': instruments}
+ return artist_dict
+
+
+def create_artist_data(release_id, options, log_options, tm, relations,
+ relation_type, artist_type, artists, instruments):
+ """
+ Update the artists and instruments
+ :param release_id: the current album id
+ :param options:
+ :param log_options:
+ :param tm: track metadata
+ :param relations:
+ :param relation_type: release', 'recording' or 'work' (NB 'work' does not pass a param for tm)
+ :param artist_type: from const.RELATION_TYPES[relation_type]
+ :param artists: current artist list - updated with each call
+ :param instruments: current instruments list - updated with each call
+ :return: artists, instruments
+ """
+ type_list = parse_data(
+ release_id,
+ relations,
+ [],
+ 'target-type:artist',
+ 'type:' +
+ artist_type)
+ for type_item in type_list:
+ artist_name_list = parse_data(
+ release_id, type_item, [], 'artist', 'name')
+ artist_sort_name_list = parse_data(
+ release_id, type_item, [], 'artist', 'sort-name')
+ if artist_type not in [
+ 'instrument',
+ 'vocal',
+ 'instrument arranger',
+ 'vocal arranger']:
+ instrument_list = None
+ credited_inst_list = None
+ else:
+ instrument_list_list = parse_data(
+ release_id, type_item, [], 'attributes')
+ if instrument_list_list:
+ instrument_list = instrument_list_list[0]
+ else:
+ instrument_list = []
+ credited_inst_list = instrument_list[:]
+ credited_inst_dict_list = parse_data(
+ release_id, type_item, [], 'attribute-credits') # keyed to insts
+ if credited_inst_dict_list:
+ credited_inst_dict = credited_inst_dict_list[0]
+ else:
+ credited_inst_dict = {}
+ for i, inst in enumerate(instrument_list):
+ if inst in credited_inst_dict:
+ credited_inst_list[i] = credited_inst_dict[inst]
+
+ if artist_type == 'vocal':
+ if not instrument_list:
+ instrument_list = ['vocals']
+ elif not any('vocals' in x for x in instrument_list):
+ instrument_list.append('vocals')
+ credited_inst_list.append('vocals')
+ # fill the hidden vars before we choose to use the as-credited
+ # version
+ if relation_type != 'work':
+ inst_tag = []
+ cred_tag = []
+ if instrument_list:
+ inst_tag = list(set(instrument_list))
+ if credited_inst_list:
+ cred_tag = list(set(credited_inst_list))
+ for attrib in ['solo', 'guest', 'additional']:
+ if attrib in inst_tag:
+ inst_tag.remove(attrib)
+ if attrib in cred_tag:
+ cred_tag.remove(attrib)
+ if inst_tag:
+ if tm['~cea_instruments']:
+ tm['~cea_instruments'] = add_list_uniquely(
+ tm['~cea_instruments'], inst_tag)
+ else:
+ tm['~cea_instruments'] = inst_tag
+ if cred_tag:
+ if tm['~cea_instruments_credited']:
+ tm['~cea_instruments_credited'] = add_list_uniquely(
+ tm['~cea_instruments_credited'], cred_tag)
+ else:
+ tm['~cea_instruments_credited'] = cred_tag
+ if inst_tag or cred_tag:
+ if tm['~cea_instruments_all']:
+ tm['~cea_instruments_all'] = add_list_uniquely(
+ tm['~cea_instruments_all'], list(set(inst_tag + cred_tag)))
+ else:
+ tm['~cea_instruments_all'] = list(
+ set(inst_tag + cred_tag))
+ if '~cea_instruments' in tm and '~cea_instruments_credited' in tm and '~cea_instruments_all' in tm:
+ instruments = [
+ tm['~cea_instruments'],
+ tm['~cea_instruments_credited'],
+ tm['~cea_instruments_all']]
+ if options['cea_inst_credit'] and credited_inst_list:
+ instrument_list = credited_inst_list
+ if instrument_list:
+ instrument_sort = 3
+ s_key = {
+ 'lead vocals': 1,
+ 'solo': 2,
+ 'guest': 4,
+ 'additional': 5}
+ for inst in s_key:
+ if inst in instrument_list:
+ instrument_sort = s_key[inst]
+ else:
+ instrument_sort = 0
+
+ if artist_type in const.ARTIST_TYPE_ORDER:
+ type_sort = const.ARTIST_TYPE_ORDER[artist_type]
+ else:
+ type_sort = 99
+ if log_options['log_error']:
+ write_log(
+ release_id,
+ 'error',
+ "Error in artist type. Type '%s' is not in ARTIST_TYPE_ORDER dictionary",
+ artist_type)
+
+ artist = (
+ artist_type,
+ instrument_list,
+ artist_name_list,
+ artist_sort_name_list,
+ instrument_sort,
+ type_sort)
+ artists.append(artist)
+ # Sorted by sort name then instrument_sort then artist type
+ artists = sorted(artists, key=lambda x: (x[5], x[3], x[4], x[1]))
+ if log_options['log_info']:
+ write_log(release_id, 'info', 'sorted artists = %s', artists)
+ return artists, instruments
+
+
+def get_series(options, release_id, relations):
+ """
+ Get series info (depends on lookup having used inc=series-rel)
+ :param options:
+ :param release_id:
+ :param relations:
+ :return:
+ """
+ # if options['log_debug'] or options['log_info']:
+ # write_log(
+ # release_id,
+ # 'debug',
+ # 'In get_series. relations: %s',
+ # relations)
+ # series_name_list =[]
+ # series_id_list = []
+ # for series_rels in relations:
+ # series_rel = parse_data(
+ # release_id,
+ # series_rels,
+ # [],
+ # 'target-type:series',
+ # 'type:part-of')
+ # if options['log_debug'] or options['log_info']:
+ # write_log(
+ # release_id,
+ # 'debug',
+ # 'series_rel = %s',
+ # series_rel)
+ # series_name_list.extend(
+ # parse_data(release_id, series_rel, [], 'series', 'name')
+ # )
+ # series_id_list.extend(
+ # parse_data(release_id, series_rel, [], 'series', 'id')
+ # )
+ type_list = parse_data(
+ release_id,
+ relations,
+ [],
+ 'target-type:series',
+ 'type:part of')
+ if type_list:
+ for type_item in type_list:
+ series_name_list = parse_data(
+ release_id, type_item, [], 'series', 'name')
+ series_id_list = parse_data(
+ release_id, type_item, [], 'series', 'id')
+ series_number_list = parse_data(
+ release_id, type_item, [], 'attribute-values', 'number')
+ return {'name_list': series_name_list, 'id_list': series_id_list, 'number_list': series_number_list}
+ else:
+ return None
+
+
+
+def apply_artist_style(
+ options,
+ release_id,
+ lang,
+ a_list,
+ name_style,
+ name_tag,
+ sort_tag,
+ names_tag,
+ names_sort_tag):
+ # Get artist and apply style
+ for a_item in a_list:
+ for acs in a_item:
+ artistlist = parse_data(release_id, acs, [], 'name')
+ sortlist = parse_data(release_id, acs, [], 'artist', 'sort-name')
+ names = {}
+ if lang:
+ names['alias'] = parse_data(
+ release_id,
+ acs,
+ [],
+ 'artist',
+ 'aliases',
+ 'locale:' + lang,
+ 'primary:True',
+ 'name')
+ else:
+ names['alias'] = []
+ names['credit'] = parse_data(release_id, acs, [], 'name')
+ pairslist = list(zip(artistlist, sortlist))
+ names['sort'] = [
+ translate_from_sortname(
+ *pair) for pair in pairslist]
+ for style in name_style:
+ if names[style]:
+ artistlist = names[style]
+ break
+ joinlist = parse_data(release_id, acs, [], 'joinphrase')
+
+ if artistlist:
+ name_tag.append(artistlist[0])
+ sort_tag.append(sortlist[0])
+ names_tag.append(artistlist[0])
+ names_sort_tag.append(sortlist[0])
+
+ if joinlist:
+ name_tag.append(joinlist[0])
+ sort_tag.append(joinlist[0])
+
+ name_tag_str = ''.join(name_tag)
+ sort_tag_str = ''.join(sort_tag)
+
+ return {
+ 'artists': names_tag,
+ 'artists_sort': names_sort_tag,
+ 'artist': name_tag_str,
+ 'artistsort': sort_tag_str}
+
+
+def set_work_artists(self, release_id, album, track, writerList, tm, count):
+ """
+ :param release_id:
+ :param self is the calling object from Artists or WorkParts
+ :param album: the current album
+ :param track: the current track
+ :param writerList: format [(artist_type, [instrument_list], [name list],[sort_name list]),(.....etc]
+ :param tm: track metadata
+ :param count: depth count of recursion in process_work_artists (should equate to part level)
+ :return:
+ """
+
+ options = self.options[track]
+ if not options['classical_work_parts']:
+ caller = 'ExtraArtists'
+ pre = '~cea'
+ else:
+ caller = 'PartLevels'
+ pre = '~cwp'
+ write_log(
+ release_id,
+ 'debug',
+ 'Class: %s: in set_work_artists for track %s. Count (level) is %s. Writer list is %s',
+ caller,
+ track,
+ count,
+ writerList)
+ # tag strings are a tuple (Picard tag, cwp tag, Picard sort tag, cwp sort
+ # tag) (NB this is modelled on set_performer)
+ tag_strings = const.tag_strings(pre)
+ # insertions lists artist types where names in the main Picard tags may be
+ # updated for annotations
+ insertions = const.INSERTIONS
+ no_more_lyricists = False
+ if caller == 'PartLevels' and self.lyricist_filled[track]:
+ no_more_lyricists = True
+
+ for writer in writerList:
+ writer_type = writer[0]
+ if writer_type not in tag_strings:
+ break
+ if no_more_lyricists and (
+ writer_type == 'lyricist' or writer_type == 'librettist'):
+ break
+ if writer[1]:
+ inst_list = writer[1][:]
+ # take a copy of the list in case (because of list
+ # mutability) we need the old one
+ instrument = ", ".join(inst_list)
+ else:
+ instrument = None
+ sub_strings = { # 'instrument arranger': instrument, 'vocal arranger': instrument
+ }
+ if options['cea_arranger']:
+ if instrument:
+ arr_inst = options['cea_arranger'] + ' ' + instrument
+ else:
+ arr_inst = options['cea_arranger']
+ else:
+ arr_inst = instrument
+ annotations = {'writer': options['cea_writer'],
+ 'lyricist': options['cea_lyricist'],
+ 'librettist': options['cea_librettist'],
+ 'revised by': options['cea_revised'],
+ 'translator': options['cea_translator'],
+ 'arranger': options['cea_arranger'],
+ 'reconstructed by': options['cea_reconstructed'],
+ 'orchestrator': options['cea_orchestrator'],
+ 'instrument arranger': arr_inst,
+ 'vocal arranger': arr_inst}
+ tag = tag_strings[writer_type][0]
+ sort_tag = tag_strings[writer_type][2]
+ cwp_tag = tag_strings[writer_type][1]
+ cwp_sort_tag = tag_strings[writer_type][3]
+ cwp_names_tag = cwp_tag[:-1] + '_names'
+ cwp_instrumented_tag = cwp_names_tag + '_instrumented'
+ if writer_type in sub_strings:
+ if sub_strings[writer_type]:
+ tag += sub_strings[writer_type]
+ if tag:
+ if '~ce_tag_cleared_' + \
+ tag not in tm or not tm['~ce_tag_cleared_' + tag] == "Y":
+ if tag in tm:
+ if options['log_info']:
+ write_log(release_id, 'info', 'delete tag %s', tag)
+ del tm[tag]
+ tm['~ce_tag_cleared_' + tag] = "Y"
+ if sort_tag:
+ if '~ce_tag_cleared_' + \
+ sort_tag not in tm or not tm['~ce_tag_cleared_' + sort_tag] == "Y":
+ if sort_tag in tm:
+ del tm[sort_tag]
+ tm['~ce_tag_cleared_' + sort_tag] = "Y"
+
+ name_list = writer[2]
+ for ind, name in enumerate(name_list):
+ sort_name = writer[3][ind]
+ no_credit = True
+ write_log(
+ release_id,
+ 'info',
+ 'In set_work_artists. Name before changes = %s',
+ name)
+ # change name to as-credited
+ if options['cea_composer_credited']:
+ if album in self.artist_credits and sort_name in self.artist_credits[album]:
+ no_credit = False
+ name = self.artist_credits[album][sort_name]
+ # over-ride with aliases if appropriate
+ if (options['cea_aliases'] or options['cea_aliases_composer']) and (
+ no_credit or options['cea_alias_overrides']):
+ if sort_name in self.artist_aliases:
+ name = self.artist_aliases[sort_name]
+ # fix cyrillic names if not already fixed
+ if options['cea_cyrillic']:
+ if not only_roman_chars(name):
+ name = remove_middle(unsort(sort_name))
+ # Only remove middle name where the existing
+ # performer is in non-latin script
+ annotated_name = name
+ write_log(
+ release_id,
+ 'info',
+ 'In set_work_artists. Name after changes = %s',
+ name)
+ # add annotations and write performer tags
+ if writer_type in annotations:
+ if annotations[writer_type]:
+ annotated_name += ' (' + annotations[writer_type] + ')'
+ if instrument:
+ instrumented_name = name + ' (' + instrument + ')'
+ else:
+ instrumented_name = name
+
+ if writer_type in insertions and options['cea_arrangers']:
+ self.append_tag(release_id, tm, tag, annotated_name)
+ else:
+ if options['cea_arrangers'] or writer_type == tag:
+ self.append_tag(release_id, tm, tag, name)
+
+ if options['cea_arrangers'] or writer_type == tag:
+ if sort_tag:
+ self.append_tag(release_id, tm, sort_tag, sort_name)
+ if options['cea_tag_sort'] and '~' in sort_tag:
+ explicit_sort_tag = sort_tag.replace('~', '')
+ self.append_tag(
+ release_id, tm, explicit_sort_tag, sort_name)
+ self.append_tag(release_id, tm, cwp_tag, annotated_name)
+ self.append_tag(release_id, tm, cwp_names_tag, name)
+ if instrumented_name != name:
+ self.append_tag(
+ release_id,
+ tm,
+ cwp_instrumented_tag,
+ instrumented_name)
+
+ if cwp_sort_tag:
+ self.append_tag(release_id, tm, cwp_sort_tag, sort_name)
+
+ if caller == 'PartLevels' and (
+ writer_type == 'lyricist' or writer_type == 'librettist'):
+ self.lyricist_filled[track] = True
+ write_log(
+ release_id,
+ 'info',
+ 'Filled lyricist for track %s. Not looking further',
+ track)
+
+ if writer_type == 'composer':
+ composerlast = sort_name.split(",")[0]
+ write_log(
+ release_id,
+ 'info',
+ 'composerlast = %s',
+ composerlast)
+ self.append_tag(
+ release_id,
+ tm,
+ pre +
+ '_composer_lastnames',
+ composerlast)
+ if sort_name in self.release_artists_sort[album]:
+ self.append_tag(
+ release_id, tm, '~cea_album_composers', name)
+ self.append_tag(
+ release_id, tm, '~cea_album_composers_sort', sort_name)
+ self.append_tag(
+ release_id,
+ tm,
+ '~cea_album_track_composer_lastnames',
+ composerlast)
+ composer_last_names(self, release_id, tm, album)
+
+
+# Non-Latin character processing
+latin_letters = {}
+
+def is_latin(uchr):
+ """Test whether character is in Latin script"""
+ try:
+ return latin_letters[uchr]
+ except KeyError:
+ return latin_letters.setdefault(
+ uchr, 'LATIN' in unicodedata.name(uchr))
+
+
+def only_roman_chars(unistr):
+ """Test whether string is in Latin script"""
+ return all(is_latin(uchr)
+ for uchr in unistr
+ if uchr.isalpha())
+
+
+def get_roman(string):
+ """Transliterate cyrillic script to Latin script"""
+ translit_string = ""
+ for index, char in enumerate(string):
+ if char in const.CYRILLIC_LOWER.keys():
+ char = const.CYRILLIC_LOWER[char]
+ elif char in const.CYRILLIC_UPPER.keys():
+ char = const.CYRILLIC_UPPER[char]
+ if string[index + 1] not in const.CYRILLIC_LOWER.keys():
+ char = char.upper()
+ translit_string += char
+ # fix multi-chars
+ translit_string = translit_string.replace('ks', 'x').replace('iy ', 'i ')
+ return translit_string
+
+
+def remove_middle(performer):
+ """To remove middle names of Russian composers"""
+ plist = performer.split()
+ if len(plist) == 3:
+ return plist[0] + ' ' + plist[2]
+ else:
+ return performer
+
+
+# Sorting etc.
+
+def unsort(performer):
+ """
+ To take a sort field and recreate the name
+ Only now used for last-ditch cyrillic translation - superseded by 'translate_from_sortname'
+ """
+ sorted_list = performer.split(', ')
+ sorted_list.reverse()
+ for i, item in enumerate(sorted_list):
+ if item[-1] != "'":
+ sorted_list[i] += ' '
+ return ''.join(sorted_list).strip()
+
+
+def _reverse_sortname(sortname):
+ """
+ Reverse sortnames.
+ Code is from picard/util/__init__.py
+ """
+
+ chunks = [a.strip() for a in sortname.split(",")]
+ chunk_len = len(chunks)
+ if chunk_len == 2:
+ return "%s %s" % (chunks[1], chunks[0])
+ elif chunk_len == 3:
+ return "%s %s %s" % (chunks[2], chunks[1], chunks[0])
+ elif chunk_len == 4:
+ return "%s %s, %s %s" % (chunks[1], chunks[0], chunks[3], chunks[2])
+ else:
+ return sortname.strip()
+
+
+def stripsir(performer):
+ """
+ Remove honorifics from names
+ Also standardize hyphens and apostrophes in names
+ """
+ performer = performer.replace(u'\u2010', u'-').replace(u'\u2019', u"'")
+ sir = re.compile(r'(.*)\b(Sir|Maestro|Dame)\b\s*(.*)', re.IGNORECASE)
+ match = sir.search(performer)
+ if match:
+ return match.group(1) + match.group(3)
+ else:
+ return performer
+
+
+# def swap_prefix(performer):
+# """NOT CURRENTLY USED. Create sort fields for ensembles etc., by placing the prefix (see constants) at the end"""
+# prefix = '|'.join(prefixes)
+# swap = re.compile(r'^(' + prefix + r')\b\s*(.*)', re.IGNORECASE)
+# match = swap.search(performer)
+# if match:
+# return match.group(2) + ", " + match.group(1)
+# else:
+# return performer
+
+
+def replace_roman_numerals(s):
+ """Replaces roman numerals include in s, where followed by certain punctuation, by digits"""
+ romans = RE_ROMANS.findall(s)
+ for roman in romans:
+ if roman[0]:
+ numerals = str(roman[0])
+ digits = str(from_roman(numerals))
+ to_replace = r'\b' + roman[0] + r'\b'
+ s = re.sub(to_replace, digits, s)
+ return s
+
+
+def from_roman(s):
+ romanNumeralMap = (('M', 1000),
+ ('CM', 900),
+ ('D', 500),
+ ('CD', 400),
+ ('C', 100),
+ ('XC', 90),
+ ('L', 50),
+ ('XL', 40),
+ ('X', 10),
+ ('IX', 9),
+ ('V', 5),
+ ('IV', 4),
+ ('I', 1),
+ ('m', 1000),
+ ('cm', 900),
+ ('d', 500),
+ ('cd', 400),
+ ('c', 100),
+ ('xc', 90),
+ ('l', 50),
+ ('xl', 40),
+ ('x', 10),
+ ('ix', 9),
+ ('v', 5),
+ ('iv', 4),
+ ('i', 1))
+ result = 0
+ index = 0
+ for numeral, integer in romanNumeralMap:
+ while s[index:index + len(numeral)] == numeral:
+ result += integer
+ index += len(numeral)
+ return result
+
+
+def turbo_lcs(release_id, multi_list):
+ """
+ Picks the best longest common string method to use
+ Works with a list of lists or a list of strings
+ :param release_id:
+ :param multi_list: a list of strings or a list of lists
+ :return: longest common substring/list
+ """
+ write_log(release_id, 'debug', 'In turbo_lcs')
+ if not isinstance(multi_list, list):
+ return None
+ list_sum = sum([len(x) for x in multi_list])
+ list_len = len(multi_list)
+ if list_len < 2:
+ if list_len == 1:
+ return multi_list[0] # Nothing to do!
+ else:
+ return []
+ # for big matches, use the generalised suffix tree method
+ if ((list_sum / list_len) ** 2) * list_len > 1000:
+ # heuristic: may need to tweak the 1000 in the light of results
+ lcs_dict = suffixtree.multi_lcs(multi_list)
+ # NB suffixtree may be shown as an unresolved reference in the IDE,
+ # but it should work provided it is included in the package
+ if "error" not in lcs_dict:
+ if "response" in lcs_dict:
+ write_log(
+ release_id,
+ 'info',
+ 'Longest common string was returned from suffix tree algo')
+ return lcs_dict['response']
+ else:
+ write_log(
+ release_id,
+ 'error',
+ 'Suffix tree failure for release %s. Error unknown. Using standard lcs algo instead',
+ release_id)
+ else:
+ write_log(
+ release_id,
+ 'error',
+ 'Suffix tree failure for release %s. Error message: %s. Using standard lcs algo instead',
+ release_id,
+ lcs_dict['error'])
+ # otherwise, or if gst fails, use the standard algorithm
+ first = True
+ common = []
+ for item in multi_list:
+ if first:
+ common = item
+ first = False
+ else:
+ lcs = longest_common_substring(
+ item, common)
+ common = lcs['string']
+ write_log(release_id, 'debug', 'LCS returned from standard algo')
+ return common
+
+
+def longest_common_substring(s1, s2):
+ """
+ Standard lcs algo for short strings, or if suffix tree does not work
+ :param s1: substring 1
+ :param s2: substring 2
+ :return: {'string': the longest common substring,
+ 'start': the start position in s1,
+ 'length': the length of the common substring}
+ NB this also works on list arguments - i.e. it will find the longest common sub-list
+ """
+ m = [[0] * (1 + len(s2)) for i in range(1 + len(s1))]
+ longest, x_longest = 0, 0
+ for x in range(1, 1 + len(s1)):
+ for y in range(1, 1 + len(s2)):
+ if s1[x - 1] == s2[y - 1]:
+ m[x][y] = m[x - 1][y - 1] + 1
+ if m[x][y] > longest:
+ longest = m[x][y]
+ x_longest = x
+ else:
+ m[x][y] = 0
+ return {'string': s1[x_longest - longest: x_longest],
+ 'start': x_longest - longest, 'length': longest}
+
+
+def longest_common_sequence(list1, list2, minstart=0, maxstart=0):
+ """
+ :param list1: list 1
+ :param list2: list 2
+ :param minstart: the earliest point to start looking for a match
+ :param maxstart: the latest point to start looking for a match
+ :return: {'sequence': the common subsequence, 'length': length of subsequence}
+ maxstart must be >= minstart. If they are equal then the start point is fixed.
+ Note that this only finds subsequences starting at the same position
+ Use longest_common_substring for the more general problem
+ """
+ if maxstart < minstart:
+ return None, 0
+ min_len = min(len(list1), len(list2))
+ longest = 0
+ seq = None
+ maxstart = min(maxstart, min_len) + 1
+ for k in range(minstart, maxstart):
+ for i in range(k, min_len + 1):
+ if list1[k:i] == list2[k:i] and i - k > longest:
+ longest = i - k
+ seq = list1[k:i]
+ return {'sequence': seq, 'length': longest}
+
+
+def substart_finder(mylist, pattern):
+ for i, list_item in enumerate(mylist):
+ if list_item == pattern[0] and mylist[i:i + len(pattern)] == pattern:
+ return i
+ return len(mylist) # if nothing found
+
+
+def get_ui_tags():
+## Determine tags for display in ui
+ options = config.setting
+ ui_tags_raw = options['ce_ui_tags']
+ ui_tags = {}
+ ui_tags_split = [x.replace('(','').strip(') ') for x in ui_tags_raw.split('/')]
+ for ui_column in ui_tags_split:
+ if ':' in ui_column:
+ ui_col_parts = [x.strip() for x in ui_column.split(':')]
+ heading = ui_col_parts[0]
+ tag_names = ui_col_parts[1].split(',')
+ tag_names = [x.strip() for x in tag_names]
+ ui_tags[heading] = tuple(tag_names)
+ return ui_tags
+
+
+def map_tags(options, release_id, album, tm):
+ """
+ Do the common tag processing - including for the genres and tag-mapping sections
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param options: options passed from either Artists or Workparts
+ :param album:
+ :param tm: track metadata
+ :return: None - action is through setting tm contents
+ This is a common function for Artists and Workparts which should only run after both sections have completed for
+ a given track. If, say, Artists calls it and Workparts is not done,
+ then it will not execute until Workparts calls it (and vice versa).
+ """
+
+ write_log(release_id, 'debug', 'In map_tags, checking readiness...')
+ if (options['classical_extra_artists'] and '~cea_artists_complete' not in tm) or (
+ options['classical_work_parts'] and '~cea_works_complete' not in tm):
+ write_log(release_id, 'info', '...not ready')
+ return
+ write_log(release_id, 'debug', '... processing tag mapping')
+
+ # blank tags
+ blank_tags = options['cea_blank_tag'].split(
+ ",") + options['cea_blank_tag_2'].split(",")
+ if 'artists_sort' in [x.strip() for x in blank_tags]:
+ blank_tags.append('~artists_sort')
+ for tag in blank_tags:
+ if tag.strip() in tm:
+ # place blanked tags into hidden variables available for
+ # re-use
+ tm['~cea_' + tag.strip()] = tm[tag.strip()]
+ del tm[tag.strip()]
+
+ # album
+ if tm['~cea_album_composer_lastnames']:
+ last_names = str_to_list(tm['~cea_album_composer_lastnames'])
+ if options['cea_composer_album']:
+ # save it as a list to prevent splitting when appending tag
+ tm['~cea_release'] = [tm['album']]
+ new_last_names = []
+ for last_name in last_names:
+ last_name = last_name.strip()
+ new_last_names.append(last_name)
+ if len(new_last_names) > 0:
+ tm['album'] = "; ".join(new_last_names) + ": " + tm['album']
+
+ # remove lyricists if no vocals, according to option set
+ if options['cea_no_lyricists'] and not any(
+ [x for x in str_to_list(tm['~cea_performers']) if 'vocals' in x]):
+ if 'lyricist' in tm:
+ del tm['lyricist']
+ for lyricist_tag in ['lyricists', 'librettists', 'translators']:
+ if '~cwp_' + lyricist_tag in tm:
+ del tm['~cwp_' + lyricist_tag]
+
+ # genres
+ if config.setting['folksonomy_tags'] and 'genre' in tm:
+ candidate_genres = str_to_list(tm['genre'])
+ append_tag(release_id, tm, '~cea_candidate_genres', candidate_genres)
+ # to avoid confusion as it will contain unmatched folksonomy tags
+ del tm['genre']
+ else:
+ candidate_genres = []
+ is_classical = False
+ composers_not_found = []
+ composer_found = False
+ composer_born_list = []
+ composer_died_list = []
+ arrangers_not_found = []
+ arranger_found = False
+ arranger_born_list = []
+ arranger_died_list = []
+ no_composer_in_metadata = False
+ if options['cwp_use_muso_refdb'] and options['cwp_muso_classical'] or options['cwp_muso_dates']:
+ if COMPOSER_DICT:
+ if '~cwp_composer_names' in tm:
+ composer_list = str_to_list(tm['~cwp_composer_names'])
+ else:
+ # maybe there were no works linked,
+ # but it might still a classical track (based on composer name)
+ no_composer_in_metadata = True
+ composer_list = str_to_list(tm['artists'])
+ composersort_list = str_to_list(tm['~artists_sort'])
+ write_log(release_id, 'info', "No composer metadata for track %s. Using artists %r", tm['title'],
+ composer_list)
+ lc_composer_list = [c.lower() for c in composer_list]
+ for ind, composer in enumerate(lc_composer_list):
+ for classical_composer in COMPOSER_DICT:
+ if composer in classical_composer['lc_name']:
+ if options['cwp_muso_classical']:
+ candidate_genres.append('Classical')
+ is_classical = True
+ if options['cwp_muso_dates']:
+ composer_born_list = classical_composer['birth']
+ composer_died_list = classical_composer['death']
+ composer_found = True
+ if no_composer_in_metadata:
+ composersort = composersort_list[ind]
+ append_tag(release_id, tm, 'composer', composer_list[ind])
+ append_tag(release_id, tm, '~cwp_composer_names', composer_list[ind])
+ append_tag(release_id, tm, 'composersort', composersort)
+ append_tag(release_id, tm, '~cwp_composers_sort', composersort)
+ append_tag(release_id, tm, '~cwp_composer_lastnames', composersort.split(', ')[0])
+ break
+ if not composer_found:
+ composer_index = lc_composer_list.index(composer)
+ orig_composer = composer_list[composer_index]
+ composers_not_found.append(orig_composer)
+ append_tag(
+ release_id,
+ tm,
+ '~cwp_unrostered_composers',
+ orig_composer)
+ if composers_not_found:
+ append_tag(
+ release_id,
+ tm,
+ '003_information:composers',
+ 'Composer(s) ' +
+ list_to_str(composers_not_found) +
+ ' not found in reference database of classical composers')
+
+ # do the same for arrangers, if required
+ if options['cwp_genres_arranger_as_composer'] or options['cwp_periods_arranger_as_composer']:
+ arranger_list = str_to_list(
+ tm['~cea_arranger_names']) + str_to_list(tm['~cwp_arranger_names'])
+ lc_arranger_list = [c.lower() for c in arranger_list]
+ for arranger in lc_arranger_list:
+ for classical_arranger in COMPOSER_DICT:
+ if arranger in classical_arranger['lc_name']:
+ if options['cwp_muso_classical'] and options['cwp_genres_arranger_as_composer']:
+ candidate_genres.append('Classical')
+ is_classical = True
+ if options['cwp_muso_dates'] and options['cwp_periods_arranger_as_composer']:
+ arranger_born_list = classical_arranger['birth']
+ arranger_died_list = classical_arranger['death']
+ arranger_found = True
+ break
+ if not arranger_found:
+ arranger_index = lc_arranger_list.index(arranger)
+ orig_arranger = arranger_list[arranger_index]
+ arrangers_not_found.append(orig_arranger)
+ append_tag(
+ release_id,
+ tm,
+ '~cwp_unrostered_arrangers',
+ orig_arranger)
+ if arrangers_not_found:
+ append_tag(
+ release_id,
+ tm,
+ '003_information:arrangers',
+ 'Arranger(s) ' +
+ list_to_str(arrangers_not_found) +
+ ' not found in reference database of classical composers')
+
+ else:
+ append_tag(
+ release_id,
+ tm,
+ '001_errors:8',
+ '8. No composer reference file. Check log for error messages re path name.')
+
+ if options['cwp_use_muso_refdb'] and options['cwp_muso_genres'] and GENRE_DICT:
+ main_classical_genres_list = [list_to_str(
+ mg['name']).strip() for mg in GENRE_DICT]
+ else:
+ main_classical_genres_list = [
+ sg.strip() for sg in options['cwp_genres_classical_main'].split(',')]
+ sub_classical_genres_list = [
+ sg.strip() for sg in options['cwp_genres_classical_sub'].split(',')]
+ main_other_genres_list = [
+ sg.strip() for sg in options['cwp_genres_other_main'].split(',')]
+ sub_other_genres_list = [sg.strip()
+ for sg in options['cwp_genres_other_sub'].split(',')]
+ main_classical_genres = []
+ sub_classical_genres = []
+ main_other_genres = []
+ sub_other_genres = []
+ if '~cea_work_type' in tm:
+ candidate_genres += str_to_list(tm['~cea_work_type'])
+ if '~cwp_candidate_genres' in tm:
+ candidate_genres += str_to_list(tm['~cwp_candidate_genres'])
+ write_log(release_id, 'info', "Candidate genres: %r", candidate_genres)
+ untagged_genres = []
+ if candidate_genres:
+ main_classical_genres = [
+ val for val in main_classical_genres_list if val.lower() in [
+ genre.lower() for genre in candidate_genres]]
+ sub_classical_genres = [
+ val for val in sub_classical_genres_list if val.lower() in [
+ genre.lower() for genre in candidate_genres]]
+
+ if main_classical_genres or sub_classical_genres or options['cwp_genres_classical_all']:
+ is_classical = True
+ main_classical_genres.append('Classical')
+ candidate_genres.append('Classical')
+ write_log(release_id, 'info', "Main classical genres for track %s: %r", tm['title'], main_classical_genres)
+ candidate_genres += str_to_list(tm['~cea_work_type_if_classical'])
+ # next two are repeated statements, but a separate fn would be
+ # clumsy too!
+ main_classical_genres = [
+ val for val in main_classical_genres_list if val.lower() in [
+ genre.lower() for genre in candidate_genres]]
+ sub_classical_genres = [
+ val for val in sub_classical_genres_list if val.lower() in [
+ genre.lower() for genre in candidate_genres]]
+ if options['cwp_genres_classical_exclude']:
+ main_classical_genres = [
+ g for g in main_classical_genres if g.lower() != 'classical']
+
+ main_other_genres = [
+ val for val in main_other_genres_list if val.lower() in [
+ genre.lower() for genre in candidate_genres]]
+ sub_other_genres = [
+ val for val in sub_other_genres_list if val.lower() in [
+ genre.lower() for genre in candidate_genres]]
+ all_genres = main_classical_genres + sub_classical_genres + \
+ main_other_genres + sub_other_genres
+ untagged_genres = [
+ un for un in candidate_genres if un.lower() not in [
+ genre.lower() for genre in all_genres]]
+
+ if options['cwp_genre_tag']:
+ if not options['cwp_genres_filter']:
+ append_tag(
+ release_id,
+ tm,
+ options['cwp_genre_tag'],
+ candidate_genres)
+ else:
+ append_tag(
+ release_id,
+ tm,
+ options['cwp_genre_tag'],
+ main_classical_genres +
+ main_other_genres)
+ if options['cwp_subgenre_tag'] and options['cwp_genres_filter']:
+ append_tag(
+ release_id,
+ tm,
+ options['cwp_subgenre_tag'],
+ sub_classical_genres +
+ sub_other_genres)
+ if is_classical and options['cwp_genres_flag_text'] and options['cwp_genres_flag_tag']:
+ tm[options['cwp_genres_flag_tag']] = options['cwp_genres_flag_text']
+ if not (
+ main_classical_genres +
+ main_other_genres)and options['cwp_genres_filter']:
+ if options['cwp_genres_default']:
+ append_tag(
+ release_id,
+ tm,
+ options['cwp_genre_tag'],
+ options['cwp_genres_default'])
+ else:
+ if options['cwp_genre_tag'] in tm:
+ del tm[options['cwp_genre_tag']]
+ if untagged_genres and options['cwp_genres_filter']:
+ append_tag(
+ release_id,
+ tm,
+ '003_information:genres',
+ 'Candidate genres found but not matched: ' +
+ list_to_str(untagged_genres))
+ append_tag(release_id, tm, '~cwp_untagged_genres', untagged_genres)
+
+ # instruments and keys
+ if options['cwp_instruments_MB_names'] and options['cwp_instruments_credited_names'] and tm['~cea_instruments_all']:
+ instruments = str_to_list(['~cea_instruments_all'])
+ elif options['cwp_instruments_MB_names'] and tm['~cea_instruments']:
+ instruments = str_to_list(tm['~cea_instruments'])
+ elif options['cwp_instruments_credited_names'] and tm['~cea_instruments_credited']:
+ instruments = str_to_list(tm['~cea_instruments_credited'])
+ else:
+ instruments = None
+ if instruments and options['cwp_instruments_tag']:
+ append_tag(release_id, tm, options['cwp_instruments_tag'], instruments)
+ # need to append rather than over-write as it may be the same as
+ # another tag (e.g. genre)
+ if tm['~cwp_keys'] and options['cwp_key_tag']:
+ append_tag(release_id, tm, options['cwp_key_tag'], tm['~cwp_keys'])
+ # dates
+ if options['cwp_workdate_annotate']:
+ comp = ' (composed)'
+ publ = ' (published)'
+ prem = ' (premiered)'
+ else:
+ comp = ''
+ publ = ''
+ prem = ''
+ tm[options['cwp_workdate_tag']] = ''
+ earliest_date = 9999
+ latest_date = -9999
+ found = False
+ if tm['~cwp_composed_dates']:
+ composed_dates_list = str_to_list(tm['~cwp_composed_dates'])
+ if len(composed_dates_list) > 1:
+ composed_dates_list = str_to_list(
+ composed_dates_list[0]) # use dates of lowest-level work
+ earliest_date = min([int(dates.split(DATE_SEP)[0].strip())
+ for dates in composed_dates_list])
+ append_tag(
+ release_id,
+ tm,
+ options['cwp_workdate_tag'],
+ list_to_str(composed_dates_list) +
+ comp)
+ found = True
+ if tm['~cwp_published_dates'] and (
+ not found or options['cwp_workdate_use_all']):
+ if not found:
+ published_dates_list = str_to_list(tm['~cwp_published_dates'])
+ if len(published_dates_list) > 1:
+ published_dates_list = str_to_list(
+ published_dates_list[0]) # use dates of lowest-level work
+ earliest_date = min([int(dates.split(DATE_SEP)[0].strip())
+ for dates in published_dates_list])
+ append_tag(
+ release_id,
+ tm,
+ options['cwp_workdate_tag'],
+ list_to_str(published_dates_list) +
+ publ)
+ found = True
+ if tm['~cwp_premiered_dates'] and (
+ not found or options['cwp_workdate_use_all']):
+ if not found:
+ premiered_dates_list = str_to_list(tm['~cwp_premiered_dates'])
+ if len(premiered_dates_list) > 1:
+ premiered_dates_list = str_to_list(
+ premiered_dates_list[0]) # use dates of lowest-level work
+ earliest_date = min([int(dates.split(DATE_SEP)[0].strip())
+ for dates in premiered_dates_list])
+ append_tag(
+ release_id,
+ tm,
+ options['cwp_workdate_tag'],
+ list_to_str(premiered_dates_list) +
+ prem)
+
+ # periods
+ PERIODS = {}
+ if options['cwp_period_map']:
+ if options['cwp_use_muso_refdb'] and options['cwp_muso_periods'] and PERIOD_DICT:
+ for p_item in PERIOD_DICT:
+ if 'start' not in p_item or p_item['start'] == []:
+ p_item['start'] = [u'-9999']
+ if 'end' not in p_item or p_item['end'] == []:
+ p_item['end'] = [u'2525']
+ if 'name' not in p_item or p_item['name'] == []:
+ p_item['name'] = ['NOT SPECIFIED']
+ PERIODS = {list_to_str(mp['name']).strip(): (
+ list_to_str(mp['start']),
+ list_to_str(mp['end']))
+ for mp in PERIOD_DICT}
+ for period in PERIODS:
+ if PERIODS[period][0].lstrip(
+ '-').isdigit() and PERIODS[period][1].lstrip('-').isdigit():
+ PERIODS[period] = (int(PERIODS[period][0]),
+ int(PERIODS[period][1]))
+ else:
+ PERIODS[period] = (
+ 9999,
+ 'ERROR - start and/or end of ' +
+ period +
+ ' are not integers')
+
+ else:
+ periods = [p.strip() for p in options['cwp_period_map'].split(';')]
+ for p in periods:
+ p = p.split(',')
+ if len(p) == 3:
+ period = p[0].strip()
+ start = p[1].strip()
+ end = p[2].strip()
+ if start.lstrip(
+ '-').isdigit() and end.lstrip('-').isdigit():
+ PERIODS[period] = (int(start), int(end))
+ else:
+ PERIODS[period] = (
+ 9999,
+ 'ERROR - start and/or end of ' +
+ period +
+ ' are not integers')
+ else:
+ PERIODS[p[0]] = (
+ 9999, 'ERROR in period map - each item must contain 3 elements')
+ if options['cwp_period_tag'] and PERIODS:
+ if earliest_date == 9999: # i.e. no work date found
+ if options['cwp_use_muso_refdb'] and options['cwp_muso_dates']:
+ for composer_born in composer_born_list + arranger_born_list:
+ if composer_born and composer_born.isdigit():
+ birthdate = int(composer_born)
+ # productive age is taken as 20->death as per Muso
+ earliest_date = min(earliest_date, birthdate + 20)
+ for composer_died in composer_died_list + arranger_died_list:
+ if composer_died and composer_died.isdigit():
+ deathdate = int(composer_died)
+ latest_date = max(latest_date, deathdate)
+ else:
+ latest_date = datetime.now().year
+ # sort into start date order before writing tags
+ sorted_periods = collections.OrderedDict(
+ sorted(PERIODS.items(), key=lambda t: t[1]))
+ for period in sorted_periods:
+ if isinstance(
+ sorted_periods[period][1],
+ str) and 'ERROR' in sorted_periods[period][1]:
+ tm[options['cwp_period_tag']] = ''
+ append_tag(
+ release_id,
+ tm,
+ '001_errors:9',
+ '9. ' +
+ sorted_periods[period])
+ break
+ if earliest_date < 9999:
+ if sorted_periods[period][0] <= earliest_date <= sorted_periods[period][1]:
+ append_tag(
+ release_id,
+ tm,
+ options['cwp_period_tag'],
+ period)
+ if latest_date > -9999:
+ if sorted_periods[period][0] <= latest_date <= sorted_periods[period][1]:
+ append_tag(
+ release_id,
+ tm,
+ options['cwp_period_tag'],
+ period)
+
+ # generic tag mapping
+ sort_tags = options['cea_tag_sort']
+ if sort_tags:
+ tm['artists_sort'] = str_to_list(tm['~artists_sort'])
+ for i in range(0, 16):
+ tagline = options['cea_tag_' + str(i + 1)].split(",")
+ source_group = options['cea_source_' + str(i + 1)].split(",")
+ conditional = options['cea_cond_' + str(i + 1)]
+ for item, tagx in enumerate(tagline):
+ tag = tagx.strip()
+ sort = sort_suffix(tag)
+ if not conditional or tm[tag] == "":
+ for source_memberx in source_group:
+ source_member = source_memberx.strip()
+ sourceline = source_member.split("+")
+ if len(sourceline) > 1:
+ source = "\\"
+ for source_itemx in sourceline:
+ source_item = source_itemx.strip()
+ source_itema = source_itemx.lstrip()
+ write_log(
+ release_id, 'info', "Source_item: %s", source_item)
+ if "~cea_" + source_item in tm:
+ si = tm['~cea_' + source_item]
+ elif "~cwp_" + source_item in tm:
+ si = tm['~cwp_' + source_item]
+ elif source_item in tm:
+ si = tm[source_item]
+ elif len(source_itema) > 0 and source_itema[0] == "\\":
+ si = source_itema[1:]
+ else:
+ si = ""
+ if si != "" and source != "":
+ source = source + si
+ else:
+ source = ""
+ else:
+ source = sourceline[0]
+ no_names_source = re.sub('(_names)$', 's', source)
+ source_sort = sort_suffix(source)
+ write_log(
+ release_id,
+ 'info',
+ "Tag mapping: Line: %s, Source: %s, Tag: %s, no_names_source: %s, sort: %s, item %s",
+ i +
+ 1,
+ source,
+ tag,
+ no_names_source,
+ sort,
+ item)
+ if '~cea_' + source in tm or '~cwp_' + source in tm:
+ for prefix in ['~cea_', '~cwp_']:
+ if prefix + source in tm:
+ write_log(release_id, 'info', prefix)
+ append_tag(release_id, tm, tag,
+ tm[prefix + source], ['; '])
+ if sort_tags:
+ if prefix + no_names_source + source_sort in tm:
+ write_log(
+ release_id, 'info', prefix + " sort")
+ append_tag(release_id, tm, tag + sort,
+ tm[prefix + no_names_source + source_sort], ['; '])
+ elif source in tm or '~' + source in tm:
+ write_log(release_id, 'info', "Picard")
+ for p in ['', '~']:
+ if p + source in tm:
+ append_tag(release_id, tm, tag,
+ tm[p + source], ['; ', '/ '])
+ if sort_tags:
+ if "~" + source + source_sort in tm:
+ source = "~" + source
+ if source + source_sort in tm:
+ write_log(
+ release_id, 'info', "Picard sort")
+ append_tag(release_id, tm, tag + sort,
+ tm[source + source_sort], ['; ', '/ '])
+ elif len(source) > 0 and source[0] == "\\":
+ append_tag(release_id, tm, tag,
+ source[1:], ['; ', '/ '])
+ else:
+ pass
+
+ # write error messages to tags
+ if options['log_error'] and "~cea_error" in tm:
+ for error in str_to_list(tm['~cea_error']):
+ ecode = error[0]
+ append_tag(release_id, tm, '001_errors:' + ecode, error)
+ if options['log_warning'] and "~cea_warning" in tm:
+ for warning in str_to_list(tm['~cea_warning']):
+ wcode = warning[0]
+ append_tag(release_id, tm, '002_warnings:' + wcode, warning)
+
+ # delete unwanted tags
+ if not options['log_debug']:
+ if '~cea_works_complete' in tm:
+ del tm['~cea_works_complete']
+ if '~cea_artists_complete' in tm:
+ del tm['~cea_artists_complete']
+ del_list = []
+ for t in tm:
+ if 'ce_tag_cleared' in t:
+ del_list.append(t)
+ for t in del_list:
+ del tm[t]
+
+ # create hidden tags to flag differences
+ if options['ce_show_ui_tags'] and options['ce_ui_tags']:
+ for heading_name, tag_tuple in UI_TAGS: # UI_TAGS is already iterated in main routine, so no need for .items() method here
+ heading_tag = '~' + heading_name + '_VAL'
+ for tag in tag_tuple:
+ if tag[-5:] != '_DIFF':
+ append_tag(release_id, tm, heading_tag, tm[tag])
+ else:
+ tag = '~' + tag
+ mirror_tags = str_to_list((tm['~ce_mirror_tags']))
+ for mirror_tag in mirror_tags:
+ mt = interpret(mirror_tag)
+ st = str_to_list(mt)
+ (old_tag, new_tag) = tuple(st)
+ diff_name = old_tag.replace('OLD', 'DIFF')
+ if diff_name == tag and tm[old_tag] != tm[new_tag]:
+ tm[diff_name] = '*****'
+ append_tag(release_id, tm, heading_tag, '*****')
+ break
+
+ # if options over-write enabled, remove it after processing one album
+ options['ce_options_overwrite'] = False
+ config.setting['ce_options_overwrite'] = False
+ # so that options are not retained (in case of refresh with different
+ # options)
+ if '~ce_options' in tm:
+ del tm['~ce_options']
+
+ # remove any unwanted file tags
+ if '~ce_file' in tm and tm['~ce_file'] != "None":
+ music_file = tm['~ce_file']
+ orig_metadata = album.tagger.files[music_file].orig_metadata
+ if 'delete_tags' in options and options['delete_tags']:
+ warn = []
+ for delete_item in options['delete_tags']:
+ if delete_item not in tm: # keep the original for comparison if we have a new version
+ if delete_item in orig_metadata:
+ del orig_metadata[delete_item]
+ if delete_item != '002_warnings:7': # to avoid circularity!
+ warn.append(delete_item)
+ if warn and options['log_warning']:
+ append_tag(
+ release_id,
+ tm,
+ '002_warnings:7',
+ '7. Deleted tags: ' +
+ ', '.join(warn))
+ write_log(
+ release_id,
+ 'warning',
+ 'Deleted tags: ' +
+ ', '.join(warn))
+
+
+def sort_suffix(tag):
+ """To determine what sort suffix is appropriate for a given tag"""
+ if tag == "composer" or tag == "artist" or tag == "albumartist" or tag == "trackartist" or tag == "~cea_MB_artist":
+ sort = "sort"
+ else:
+ sort = "_sort"
+ return sort
+
+
+def append_tag(release_id, tm, tag, source, separators=None):
+ """
+ Update a tag
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param tm: track metadata
+ :param tag: tag to be appended to
+ :param source: item to append to tag
+ :param separators: characters which may be used to split string into a list
+ (any of the characters will be a split point)
+ :return: None. Action is on tm
+ """
+ if not separators:
+ separators = []
+ if tag and tag != "":
+ if config.setting['log_info']:
+ write_log(
+ release_id,
+ 'info',
+ 'Appending source: %r to tag: %s (source is type %s) ...',
+ source,
+ tag,
+ type(source))
+ if tag in tm:
+ write_log(
+ release_id,
+ 'info',
+ '... existing tag contents = %r',
+ tm[tag])
+ if source and len(source) > 0:
+ if isinstance(source, str):
+ if separators:
+ source = re.split('|'.join(separators), source)
+ else:
+ source = [source]
+ if not isinstance(source, list):
+ source = [source] # typically for dict items such as saved options
+ if all([isinstance(x, str) for x in source]): # only append if if source is a list of strings
+ if tag not in tm:
+ if tag == 'artists_sort':
+ # There is no artists_sort tag in Picard - just a
+ # hidden var ~artists_sort, so pick up those into the new tag
+ hidden = tm['~artists_sort']
+ if not isinstance(hidden, list):
+ if separators:
+ hidden = re.split(
+ '|'.join(separators), hidden)
+ for i, h in enumerate(hidden):
+ hidden[i] = h.strip()
+ else:
+ hidden = [hidden]
+ source = add_list_uniquely(source, hidden)
+ new_tag = True
+ else:
+ new_tag = False
+
+ for source_item in source:
+ if isinstance(source_item, str):
+ source_item = source_item.replace(u'\u2010', u'-')
+ source_item = source_item.replace(u'\u2011', u'-')
+ source_item = source_item.replace(u'\u2019', u"'")
+ source_item = source_item.replace(u'\u2018', u"'")
+ source_item = source_item.replace(u'\u201c', u'"')
+ source_item = source_item.replace(u'\u201d', u'"')
+ if new_tag:
+ tm[tag] = [source_item]
+ new_tag = False
+ else:
+ if not isinstance(tm[tag], list):
+ if separators:
+ tag_list = re.split(
+ '|'.join(separators), tm[tag])
+ for i, t in enumerate(tag_list):
+ tag_list[i] = t.strip()
+ else:
+ tag_list = [tm[tag]]
+ else:
+ tag_list = tm[tag]
+ if source_item not in tm[tag]:
+ tag_list.append(source_item)
+ tm[tag] = tag_list
+ # NB tag_list is used as metadata object will convert single-item lists to strings
+ else: # source items are not strings, so just replace
+ tm[tag] = source
+
+def get_artist_credit(options, release_id, obj):
+ """
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param options:
+ :param obj: an XmlNode
+ :return: a list of as-credited names
+ """
+ name_credit_list = parse_data(release_id, obj, [], 'artist-credit')
+ credit_list = []
+ if name_credit_list:
+ for name_credits in name_credit_list:
+ for name_credit in name_credits:
+ credited_artist = parse_data(
+ release_id, name_credit, [], 'name')
+ if credited_artist:
+ name = parse_data(
+ release_id, name_credit, [], 'artist', 'name')
+ sort_name = parse_data(
+ release_id, name_credit, [], 'artist', 'sort-name')
+ credit_item = (credited_artist, name, sort_name)
+ credit_list.append(credit_item)
+ return credit_list
+
+
+def get_aliases_and_credits(
+ self,
+ options,
+ release_id,
+ album,
+ obj,
+ lang,
+ credited):
+ """
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param album:
+ :param self: This relates to the object in the class which called this function
+ :param options:
+ :param obj: an XmlNode
+ :param lang: The language selected in the Picard metadata options
+ :param credited: The options item to determine what as-credited names are being sought
+ :return: None. Sets self.artist_aliases and self.artist_credits[album]
+ """
+ name_credit_list = parse_data(release_id, obj, [], 'artist-credit')
+ artist_list = parse_data(release_id, name_credit_list, [], 'artist')
+ for artist in artist_list:
+ sort_names = parse_data(release_id, artist, [], 'sort-name')
+ if sort_names:
+ aliases = parse_data(release_id, artist, [], 'aliases', 'locale:' +
+ lang, 'primary:True', 'name')
+ if aliases:
+ self.artist_aliases[sort_names[0]] = aliases[0]
+ if credited:
+ for name_credit in name_credit_list[0]:
+ credited_artist = parse_data(release_id, name_credit, [], 'name')
+ if credited_artist:
+ sort_name = parse_data(
+ release_id, name_credit, [], 'artist', 'sort-name')
+ if sort_name:
+ self.artist_credits[album][sort_name[0]
+ ] = credited_artist[0]
+
+
+def get_relation_credits(
+ self,
+ options,
+ release_id,
+ album,
+ obj,
+ lang,
+ credited):
+ """
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param self:
+ :param options: UI options
+ :param album: current album
+ :param obj: XmloOde
+ :return: None
+ Note that direct recording relationships will over-ride indirect ones (via work)
+ """
+
+ rels = parse_data(release_id, obj, [], 'relations', 'target-type:work',
+ 'work', 'relations', 'target-type:artist')
+
+ for artist in rels:
+ sort_names = parse_data(release_id, artist, [], 'artist', 'sort-name')
+ if sort_names:
+ credited_artists = parse_data(
+ release_id, artist, [], 'target-credit')
+ if credited_artists and credited_artists[0] != '' and credited:
+ self.artist_credits[album][sort_names[0]
+ ] = credited_artists[0]
+ aliases = parse_data(
+ release_id,
+ artist,
+ [],
+ 'artist',
+ 'aliases',
+ 'locale:' + lang,
+ 'primary:True',
+ 'name')
+ if aliases:
+ self.artist_aliases[sort_names[0]] = aliases[0]
+
+ rels2 = parse_data(release_id, obj, [], 'relations', 'target-type:artist')
+
+ for artist in rels2:
+ sort_names = parse_data(release_id, artist, [], 'artist', 'sort-name')
+ if sort_names:
+ credited_artists = parse_data(
+ release_id, artist, [], 'target-credit')
+ if credited_artists and credited_artists[0] != '' and credited:
+ self.artist_credits[album][sort_names[0]
+ ] = credited_artists[0]
+ aliases = parse_data(
+ release_id,
+ artist,
+ [],
+ 'artist',
+ 'aliases',
+ 'locale:' + lang,
+ 'primary:True',
+ 'name')
+ if aliases:
+ self.artist_aliases[sort_names[0]] = aliases[0]
+
+
+def composer_last_names(self, release_id, tm, album):
+ """
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param self:
+ :param tm:
+ :param album:
+ :return: None
+ Sets composer last names for album prefixing
+ """
+ if '~cea_album_track_composer_lastnames' in tm:
+ if not isinstance(tm['~cea_album_track_composer_lastnames'], list):
+ atc_list = re.split(
+ '|'.join(
+ self.SEPARATORS),
+ tm['~cea_album_track_composer_lastnames'])
+ else:
+ atc_list = str_to_list(tm['~cea_album_track_composer_lastnames'])
+ for atc_item in atc_list:
+ composer_lastnames = atc_item.strip()
+ if '~length' in tm and tm['~length']:
+ track_length = time_to_secs(tm['~length'])
+ else:
+ track_length = 0
+ if album in self.album_artists:
+ if 'composer_lastnames' in self.album_artists[album]:
+ if composer_lastnames not in self.album_artists[album]['composer_lastnames']:
+ self.album_artists[album]['composer_lastnames'][composer_lastnames] = {
+ 'length': track_length}
+ else:
+ self.album_artists[album]['composer_lastnames'][composer_lastnames]['length'] += track_length
+ else:
+ self.album_artists[album]['composer_lastnames'][composer_lastnames] = {
+ 'length': track_length}
+ else:
+ self.album_artists[album]['composer_lastnames'][composer_lastnames] = {
+ 'length': track_length}
+ else:
+ write_log(
+ release_id,
+ 'warning',
+ "No _cea_album_track_composer_lastnames variable available for recording \"%s\".",
+ tm['title'])
+ if 'composer' in tm:
+ self.append_tag(
+ release_id,
+ release_id,
+ tm,
+ '~cea_warning',
+ '1. Composer for this track is not in album artists and will not be available to prefix album')
+ else:
+ self.append_tag(
+ release_id,
+ release_id,
+ tm,
+ '~cea_warning',
+ '1. No composer for this track, but checking parent work.')
+
+
+def add_list_uniquely(list_to, list_from):
+ """
+ Adds any items in list_from to list_to, if they are not already present
+ If either arg is a string, it will be converted to a list, e.g. 'abc' -> ['abc']
+ :param list_to:
+ :param list_from:
+ :return: appends only unique elements of list 2 to list 1
+ """
+ #
+ if list_to and list_from:
+ if not isinstance(list_to, list):
+ list_to = str_to_list(list_to)
+ if not isinstance(list_from, list):
+ list_from = str_to_list(list_from)
+ for list_item in list_from:
+ if list_item not in list_to:
+ list_to.append(list_item)
+ else:
+ if list_from:
+ list_to = list_from
+ return list_to
+
+
+def str_to_list(s):
+ """
+ :param s:
+ :return: list from string using ; as separator
+ """
+ if isinstance(s, list):
+ return s
+ if not isinstance(s, str):
+ try:
+ return list(s)
+ except TypeError:
+ return []
+ else:
+ if s == '':
+ return []
+ else:
+ return s.split('; ')
+
+
+def list_to_str(l):
+ """
+ :param l:
+ :return: string from list using ; as separator
+ """
+ if not isinstance(l, list):
+ return l
+ else:
+ return '; '.join(l)
+
+
+def interpret(tag):
+ """
+ :param tag:
+ :return: safe form of eval(tag)
+ """
+ if isinstance(tag, str):
+ try:
+ tag = tag.strip(' \n\t')
+ return ast.literal_eval(tag)
+ except (SyntaxError, ValueError):
+ return tag
+ else:
+ return tag
+
+
+def time_to_secs(a):
+ """
+ :param a: string x:x:x
+ :return: seconds
+ converts string times to seconds
+ """
+ ax = a.split(':')
+ ax = ax[::-1]
+ t = 0
+ for i, x in enumerate(ax):
+ if x.isdigit():
+ t += int(x) * (60 ** i)
+ else:
+ return 0
+ return t
+
+
+def seq_last_names(self, album):
+ """
+ Sequences composer last names for album prefix by the total lengths of their tracks
+ :param self:
+ :param album:
+ :return:
+ """
+ ln = []
+ if album in self.album_artists and 'composer_lastnames' in self.album_artists[album]:
+ for x in self.album_artists[album]['composer_lastnames']:
+ if 'length' in self.album_artists[album]['composer_lastnames'][x]:
+ ln.append([x, self.album_artists[album]
+ ['composer_lastnames'][x]['length']])
+ else:
+ return []
+ ln = sorted(ln, key=lambda a: a[1])
+ ln = ln[::-1]
+ return [a[0] for a in ln]
+
+
+def year(date):
+ """
+ Return YYYY portion of date(s) in YYYY-MM-DD format (may be incomplete, string or list)
+ :param date:
+ :return: YYYY
+ """
+ if isinstance(date, list):
+ year_list = [blank_if_none(d).split('-')[0] for d in date]
+ return year_list
+ else:
+ date_list = blank_if_none(date).split('-')
+ return [date_list[0]]
+
+
+def blank_if_none(val):
+ """
+ Make NoneTypes strings
+ :param val: str or None
+ :return: str
+ """
+ if not val:
+ return ''
+ else:
+ return val
+
+
+def strip_excess_punctuation(s):
+ """
+ remove orphan punctuation, unmatched quotes and brackets
+ :param s: string
+ :return: string
+ """
+ if s:
+ s_prev = ''
+ counter = 0
+ while s != s_prev:
+ if counter > 100:
+ break # safety valve
+ s_prev = s
+ s = s.replace(' ', ' ')
+ s = s.strip("&.-:;, ")
+ s = s.lstrip("!)]}")
+ s = s.rstrip("([{")
+ s = s.lstrip(u"\u2019") # Right single quote
+ s = s.lstrip(u"\u201D") # Right double quote
+ if s.count(u"\u201E") == 0: # u201E is lower double quote (German etc.)
+ s = s.rstrip(u"\u201C") # Left double quote - only strip if there is no German-style lower quote present
+ s = s.rstrip(u"\u2018") # Left single quote
+ if s.count('"') % 2 != 0:
+ s = s.strip('"')
+ if s.count("'") % 2 != 0:
+ s = s.strip("'")
+ if len(s) > 0 and s[0] == u"\u201C" and s.count(u"\u201D") == 0:
+ s = s.lstrip(u"\u201C")
+ if len(s) > 0 and s[-1] == u"\u201D" and s.count(u"\u201C") == 0 and s.count(u"\u201E") == 0: # only strip if there is no German-style lower quote present
+ s = s.rstrip(u"\u201D")
+ if len(s) > 0 and s[0] == u"\u2018" and s.count(u"\u2019") == 0:
+ s = s.lstrip(u"\u2018")
+ if len(s) > 0 and s[-1] == u"\u2019" and s.count(u"\u2018") == 0:
+ s = s.rstrip(u"\u2019")
+ if s:
+ if s.count("\"") == 1:
+ s = s.replace('"', '')
+ if s.count("\'") == 1:
+ s = s.replace(" '", " ")
+ # s = s.replace("' ", " ") # removed to prevent removal of genuine apostrophes
+ if "(" in s and ")" not in s:
+ s = s.replace("(", "")
+ if ")" in s and "(" not in s:
+ s = s.replace(")", "")
+ if "[" in s and "]" not in s:
+ s = s.replace("[", "")
+ if "]" in s and "[" not in s:
+ s = s.replace("]", "")
+ if "{" in s and "}" not in s:
+ s = s.replace("{", "")
+ if "}" in s and "{" not in s:
+ s = s.replace("}", "")
+ if s:
+ match_chars = [("(", ")"), ("[", "]"), ("{", "}")]
+ last = len(s) - 1
+ for char_pair in match_chars:
+ if char_pair[0] == s[0] and char_pair[1] == s[last]:
+ s = s.lstrip(char_pair[0]).rstrip(char_pair[1])
+ counter += 1
+ return s
+
+
+#################
+#################
+# EXTRA ARTISTS #
+#################
+#################
+
+
+class ExtraArtists():
+
+ # CONSTANTS
+ def __init__(self):
+ self.album_artists = collections.defaultdict(
+ lambda: collections.defaultdict(dict))
+ # collection of artists to be applied at album level
+
+ self.track_listing = collections.defaultdict(list)
+ # collection of tracks - format is {album: [track 1,
+ # track 2, ...]}
+
+ self.options = collections.defaultdict(dict)
+ # collection of Classical Extras options
+
+ self.globals = collections.defaultdict(dict)
+ # collection of global variables for this class
+
+ self.album_performers = collections.defaultdict(
+ lambda: collections.defaultdict(dict))
+ # collection of performers who have release relationships, not track
+ # relationships
+
+ self.album_instruments = collections.defaultdict(
+ lambda: collections.defaultdict(dict))
+ # collection of instruments which have release relationships, not track
+ # relationships
+
+ self.artist_aliases = {}
+ # collection of alias names - format is {sort_name: alias_name, ...}
+
+ self.artist_credits = collections.defaultdict(dict)
+ # collection of credited-as names - format is {album: {sort_name: credit_name,
+ # ...}, ...}
+
+ self.release_artists_sort = collections.defaultdict(list)
+ # collection of release artists - format is {album: [sort_name_1,
+ # sort_name_2, ...]}
+
+ self.lyricist_filled = collections.defaultdict(dict)
+ # Boolean for each track to indicate if lyricist has been found (don't
+ # want to add more from higher levels)
+ # NB this last one is for completeness - not actually used by
+ # ExtraArtists, but here to remove pep8 error
+
+ self.album_series_list = collections.defaultdict(dict)
+ # series relationships - format is {'name_list': series names, 'id_list': series ids, 'number_list': number within series}
+
+ def add_artist_info(
+ self,
+ album,
+ track_metadata,
+ trackXmlNode,
+ releaseXmlNode):
+ """
+ Main routine run for each track of release
+ :param album: Current release
+ :param track_metadata: track metadata dictionary
+ :param trackXmlNode: Everything in the track node downwards
+ :param releaseXmlNode: Everything in the release node downwards (so includes all track nodes)
+ :return:
+ """
+ release_id = track_metadata['musicbrainz_albumid']
+ if 'start' not in release_status[release_id]:
+ release_status[release_id]['start'] = datetime.now()
+ if 'lookups' not in release_status[release_id]:
+ release_status[release_id]['lookups'] = 0
+ release_status[release_id]['name'] = track_metadata['album']
+ release_status[release_id]['artists'] = True
+ if config.setting['log_debug'] or config.setting['log_info']:
+ write_log(
+ release_id,
+ 'debug',
+ 'STARTING ARTIST PROCESSING FOR ALBUM %s, DISC %s, TRACK %s',
+ track_metadata['album'],
+ track_metadata['discnumber'],
+ track_metadata['tracknumber'] +
+ ' ' +
+ track_metadata['title'])
+ # write_log(release_id, 'info', 'trackXmlNode = %s', trackXmlNode) # NB can crash Picard
+ # write_log('info', 'releaseXmlNode = %s', releaseXmlNode) # NB can crash Picard
+ # Jump through hoops to get track object!!
+ track = album._new_tracks[-1]
+ tm = track.metadata
+
+ # OPTIONS - OVER-RIDE IF REQUIRED
+ if '~ce_options' not in tm:
+ if config.setting['log_debug'] or config.setting['log_info']:
+ write_log(release_id, 'debug', 'Artists gets track first...')
+ get_options(release_id, album, track)
+ options = interpret(tm['~ce_options'])
+ if not options:
+ if config.setting["log_error"]:
+ write_log(
+ release_id,
+ 'error',
+ 'Artists. Failure to read saved options for track %s. options = %s',
+ track,
+ tm['~ce_options'])
+ options = option_settings(config.setting)
+ self.options[track] = options
+
+ # CONSTANTS
+ self.ERROR = options["log_error"]
+ self.WARNING = options["log_warning"]
+ self.ORCHESTRAS = options["cea_orchestras"].split(',')
+ self.CHOIRS = options["cea_choirs"].split(',')
+ self.GROUPS = options["cea_groups"].split(',')
+ self.ENSEMBLE_TYPES = self.ORCHESTRAS + self.CHOIRS + self.GROUPS
+ self.SEPARATORS = ['; ', '/ ', ';', '/']
+
+ # continue?
+ if not options["classical_extra_artists"]:
+ return
+ # album_files is not used - this is just for logging
+ album_files = album.tagger.get_files_from_objects([album])
+ if options['log_info']:
+ write_log(
+ release_id,
+ 'info',
+ 'ALBUM FILENAMES for album %r = %s',
+ album,
+ album_files)
+
+ if not (
+ options["ce_no_run"] and (
+ not tm['~ce_file'] or tm['~ce_file'] == "None")):
+ # continue
+ write_log(
+ release_id,
+ 'debug',
+ "ExtraArtists - add_artist_info")
+ if album not in self.track_listing or track not in self.track_listing[album]:
+ self.track_listing[album].append(track)
+ # fix odd hyphens in names for consistency
+ field_types = ['~albumartists', '~albumartists_sort']
+ for field_type in field_types:
+ if field_type in tm:
+ field = tm[field_type]
+ if isinstance(field, list):
+ for x, it in enumerate(field):
+ field[x] = it.replace(u'\u2010', u'-')
+ elif isinstance(field, str):
+ field = field.replace(u'\u2010', u'-')
+ else:
+ pass
+ tm[field_type] = field
+
+ # first time for this album (reloads each refresh)
+ if tm['discnumber'] == '1' and tm['tracknumber'] == '1':
+ # get artist aliases - these are cached so can be re-used across
+ # releases, but are reloaded with each refresh
+ get_aliases(self, release_id, album, options, releaseXmlNode)
+
+ # xml_type = 'release'
+ # get performers etc who are related at the release level
+ relation_list = parse_data(
+ release_id, releaseXmlNode, [], 'relations')
+ album_performerList = get_artists(
+ options, release_id, tm, relation_list, 'release')['artists']
+ self.album_performers[album] = album_performerList
+ album_instrumentList = get_artists(
+ options, release_id, tm, relation_list, 'release')['instruments']
+ self.album_instruments[album] = album_instrumentList
+
+ # get series information
+ self.album_series_list = get_series(
+ options, release_id, relation_list)
+
+ else:
+ if album in self.album_performers:
+ album_performerList = self.album_performers[album]
+ else:
+ album_performerList = []
+ if album in self.album_instruments and self.album_instruments[album]:
+ tm['~cea_instruments'] = self.album_instruments[album][0]
+ tm['~cea_instruments_credited'] = self.album_instruments[album][1]
+ tm['~cea_instruments_all'] = self.album_instruments[album][2]
+ # Should be OK to initialise these here as recording artists
+ # yet to be processed
+
+ # Fill release info not given by vanilla Picard
+ if self.album_series_list:
+ tm['series'] = self.album_series_list['name_list'] if 'name_list' in self.album_series_list else None
+ tm['musicbrainz_seriesid'] = self.album_series_list['id_list'] if 'id_list' in self.album_series_list else None
+ tm['series_number'] = self.album_series_list['number_list'] if 'number_list' in self.album_series_list else None
+ ## TODO add label id too
+ recording_relation_list = parse_data(
+ release_id, trackXmlNode, [], 'recording', 'relations')
+ recording_series_list = get_series(
+ options, release_id, recording_relation_list)
+ write_log(
+ release_id,
+ 'info',
+ 'Recording_series_list = %s',
+ recording_series_list)
+
+ track_artist_list = parse_data(
+ release_id, trackXmlNode, [], 'artist-credit')
+ if track_artist_list:
+ track_artist = []
+ track_artistsort = []
+ track_artists = []
+ track_artists_sort = []
+ locale = config.setting["artist_locale"]
+ # NB this is the Picard code in /util
+ lang = locale.split("_")[0]
+
+ # Set naming option
+ # Put naming style into preferential list
+
+ # naming as for vanilla Picard for track artists
+
+ if options['translate_artist_names'] and lang:
+ name_style = ['alias', 'sort']
+ # documentation indicates that processing should be as below,
+ # but processing above appears to reflect what vanilla Picard actually does
+ # if options['standardize_artists']:
+ # name_style = ['alias', 'sort']
+ # else:
+ # name_style = ['alias', 'credit', 'sort']
+ else:
+ if not options['standardize_artists']:
+ name_style = ['credit']
+ else:
+ name_style = []
+ write_log(
+ release_id,
+ 'info',
+ 'Priority order of naming style for track artists = %s',
+ name_style)
+ styled_artists = apply_artist_style(
+ options,
+ release_id,
+ lang,
+ track_artist_list,
+ name_style,
+ track_artist,
+ track_artistsort,
+ track_artists,
+ track_artists_sort)
+ tm['artists'] = styled_artists['artists']
+ tm['~artists_sort'] = styled_artists['artists_sort']
+ tm['artist'] = styled_artists['artist']
+ tm['artistsort'] = styled_artists['artistsort']
+
+ if 'recording' in trackXmlNode:
+ self.globals[track]['is_recording'] = True
+ write_log(release_id, 'debug', 'Getting recording details')
+ recording = trackXmlNode['recording']
+ if not isinstance(recording, list):
+ recording = [recording]
+ for record in recording:
+ rec_type = type(record)
+ write_log(release_id, 'info', 'rec-type = %s', rec_type)
+ write_log(release_id, 'info', record)
+ # Note that the lists below reflect https://musicbrainz.org/relationships/artist-recording
+ # Any changes to that DB structure will require changes
+ # here
+
+ # get recording artists data
+ recording_artist_list = parse_data(
+ release_id, record, [], 'artist-credit')
+ if recording_artist_list:
+ recording_artist = []
+ recording_artistsort = []
+ recording_artists = []
+ recording_artists_sort = []
+ locale = config.setting["artist_locale"]
+ # NB this is the Picard code in /util
+ lang = locale.split("_")[0]
+
+ # Set naming option
+ # Put naming style into preferential list
+
+ # naming as for vanilla Picard for track artists (per
+ # documentation rather than actual?)
+ if options['cea_ra_trackartist']:
+ if options['translate_artist_names'] and lang:
+ if options['standardize_artists']:
+ name_style = ['alias', 'sort']
+ else:
+ name_style = ['alias', 'credit', 'sort']
+ else:
+ if not options['standardize_artists']:
+ name_style = ['credit']
+ else:
+ name_style = []
+ # naming as for performers in classical extras
+ elif options['cea_ra_performer']:
+ if options['cea_aliases']:
+ if options['cea_alias_overrides']:
+ name_style = ['alias', 'credit']
+ else:
+ name_style = ['credit', 'alias']
+ else:
+ name_style = ['credit']
+
+ else:
+ name_style = []
+ write_log(
+ release_id,
+ 'info',
+ 'Priority order of naming style for recording artists = %s',
+ name_style)
+
+ styled_artists = apply_artist_style(
+ options,
+ release_id,
+ lang,
+ recording_artist_list,
+ name_style,
+ recording_artist,
+ recording_artistsort,
+ recording_artists,
+ recording_artists_sort)
+ self.append_tag(
+ release_id,
+ tm,
+ '~cea_recording_artists',
+ styled_artists['artists'])
+ self.append_tag(
+ release_id,
+ tm,
+ '~cea_recording_artists_sort',
+ styled_artists['artists_sort'])
+ self.append_tag(
+ release_id,
+ tm,
+ '~cea_recording_artist',
+ styled_artists['artist'])
+ self.append_tag(
+ release_id,
+ tm,
+ '~cea_recording_artistsort',
+ styled_artists['artistsort'])
+
+ else:
+ tm['~cea_recording_artists'] = ''
+ tm['~cea_recording_artists_sort'] = ''
+ tm['~cea_recording_artist'] = ''
+ tm['~cea_recording_artistsort'] = ''
+
+ # use recording artist options
+ tm['~cea_MB_artist'] = str_to_list(tm['artist'])
+ tm['~cea_MB_artistsort'] = str_to_list(tm['artistsort'])
+ tm['~cea_MB_artists'] = str_to_list(tm['artists'])
+ tm['~cea_MB_artists_sort'] = str_to_list(tm['~artists_sort'])
+
+ if options['cea_ra_use']:
+ if options['cea_ra_replace_ta']:
+ if tm['~cea_recording_artist']:
+ tm['artist'] = str_to_list(tm['~cea_recording_artist'])
+ tm['artistsort'] = str_to_list(tm['~cea_recording_artistsort'])
+ tm['artists'] = str_to_list(tm['~cea_recording_artists'])
+ tm['~artists_sort'] = str_to_list(tm['~cea_recording_artists_sort'])
+ elif not options['cea_ra_noblank_ta']:
+ tm['artist'] = ''
+ tm['artistsort'] = ''
+ tm['artists'] = ''
+ tm['~artists_sort'] = ''
+ elif options['cea_ra_merge_ta']:
+ if tm['~cea_recording_artist']:
+ tm['artists'] = add_list_uniquely(
+ tm['artists'], tm['~cea_recording_artists'])
+ tm['~artists_sort'] = add_list_uniquely(
+ tm['~artists_sort'], tm['~cea_recording_artists_sort'])
+ if tm['artist'] != tm['~cea_recording_artist']:
+ tm['artist'] = tm['artist'] + \
+ ' (' + tm['~cea_recording_artist'] + ')'
+ tm['artistsort'] = tm['artistsort'] + \
+ ' (' + tm['~cea_recording_artistsort'] + ')'
+
+ # xml_type = 'recording'
+ relation_list = parse_data(
+ release_id, record, [], 'relations')
+ performerList = album_performerList + \
+ get_artists(options, release_id, tm, relation_list, 'recording')['artists']
+ # returns
+ # [(artist type, instrument or None, artist name, artist sort name, instrument sort, type sort)]
+ # where instrument sort places solo ahead of additional etc.
+ # and type sort applies a custom sequencing to the artist
+ # types
+ if performerList:
+ write_log(
+ release_id, 'info', "Performers: %s", performerList)
+ self.set_performer(
+ release_id, album, track, performerList, tm)
+ if not options['classical_work_parts']:
+ work_artist_list = parse_data(
+ release_id,
+ record,
+ [],
+ 'relations',
+ 'target-type:work',
+ 'type:performance',
+ 'work',
+ 'relations',
+ 'target-type:artist')
+ work_artists = get_artists(
+ options, release_id, tm, work_artist_list, 'work')['artists']
+ set_work_artists(
+ self, release_id, album, track, work_artists, tm, 0)
+ # otherwise composers etc. will be set in work parts
+ else:
+ self.globals[track]['is_recording'] = False
+ else:
+ tm['000_major_warning'] = "WARNING: Classical Extras not run for this track as no file present - " \
+ "deselect the option on the advanced tab to run. If there is a file, then try 'Refresh'."
+ if track_metadata['tracknumber'] == track_metadata['totaltracks'] and track_metadata[
+ 'discnumber'] == track_metadata['totaldiscs']: # last track
+ self.process_album(release_id, album)
+ release_status[release_id]['artists-done'] = datetime.now()
+ close_log(release_id, 'artists')
+
+ # Checks for ensembles
+ def ensemble_type(self, performer):
+ """
+ Returns ensemble types
+ :param performer:
+ :return:
+ """
+ for ensemble_name in self.ORCHESTRAS:
+ ensemble = re.compile(
+ r'(.*)\b' +
+ ensemble_name +
+ r'\b(.*)',
+ re.IGNORECASE)
+ if ensemble.search(performer):
+ return 'Orchestra'
+ for ensemble_name in self.CHOIRS:
+ ensemble = re.compile(
+ r'(.*)\b' +
+ ensemble_name +
+ r'\b(.*)',
+ re.IGNORECASE)
+ if ensemble.search(performer):
+ return 'Choir'
+ for ensemble_name in self.GROUPS:
+ ensemble = re.compile(
+ r'(.*)\b' +
+ ensemble_name +
+ r'\b(.*)',
+ re.IGNORECASE)
+ if ensemble.search(performer):
+ return 'Group'
+ return False
+
+ def process_album(self, release_id, album):
+ """
+ Perform final processing after all tracks read
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param album:
+ :return:
+ """
+ write_log(
+ release_id,
+ 'debug',
+ 'ExtraArtists: Starting process_album')
+ # process lyrics tags
+ write_log(release_id, 'debug', 'Starting lyrics processing')
+ common = []
+ tmlyrics_dict = {}
+ tmlyrics_sort = []
+ for track in self.track_listing[album]:
+ options = self.options[track]
+ if options['cea_split_lyrics'] and options['cea_lyrics_tag']:
+ tm = track.metadata
+ lyrics_tag = options['cea_lyrics_tag']
+ if tm[lyrics_tag]:
+ # turn text into word lists to speed processing
+ tmlyrics_dict[track] = tm[lyrics_tag].split()
+ if tmlyrics_dict:
+ tmlyrics_sort = sorted(
+ tmlyrics_dict.items(),
+ key=operator.itemgetter(1))
+ prev = None
+ first_track = None
+ unique_lyrics = []
+ ref_track = {}
+ for lyric_tuple in tmlyrics_sort: # tuple is (track, lyrics)
+ if lyric_tuple[1] != prev:
+ unique_lyrics.append(lyric_tuple[1])
+ first_track = lyric_tuple[0]
+ ref_track[lyric_tuple[0]] = first_track
+ prev = lyric_tuple[1]
+ common = turbo_lcs(
+ release_id,
+ unique_lyrics)
+
+ if common:
+ unique = []
+ for tup in tmlyrics_sort:
+ track = tup[0]
+ ref = ref_track[track]
+ if track == ref:
+ start = substart_finder(tup[1], common)
+ length = len(common)
+ end = min(start + length, len(tup[1]))
+ unique = tup[1][:start] + tup[1][end:]
+
+ options = self.options[track]
+ if options['cea_split_lyrics'] and options['cea_lyrics_tag']:
+ tm = track.metadata
+ if unique:
+ tm['~cea_track_lyrics'] = ' '.join(unique)
+ tm['~cea_album_lyrics'] = ' '.join(common)
+ if options['cea_album_lyrics']:
+ tm[options['cea_album_lyrics']] = tm['~cea_album_lyrics']
+ if unique and options['cea_track_lyrics']:
+ tm[options['cea_track_lyrics']] = tm['~cea_track_lyrics']
+ else:
+ for track in self.track_listing[album]:
+ options = self.options[track]
+ if options['cea_split_lyrics'] and options['cea_lyrics_tag']:
+ tm['~cea_track_lyrics'] = tm[options['cea_lyrics_tag']]
+ if options['cea_track_lyrics']:
+ tm[options['cea_track_lyrics']] = tm['~cea_track_lyrics']
+ write_log(release_id, 'debug', 'Ending lyrics processing')
+
+ for track in self.track_listing[album]:
+ self.write_metadata(release_id, options, album, track)
+ self.track_listing[album] = []
+ write_log(
+ release_id,
+ 'info',
+ "FINISHED Classical Extra Artists. Album: %s",
+ album)
+
+
+ def write_metadata(self, release_id, options, album, track):
+ """
+ Write the metadat for this track
+ :param release_id:
+ :param album:
+ :param track:
+ :return:
+ """
+ options = self.options[track]
+ tm = track.metadata
+ tm['~cea_version'] = PLUGIN_VERSION
+
+ # set inferred genres before any tags are blanked
+ if options['cwp_genres_infer']:
+ self.infer_genres(release_id, options, track, tm)
+
+ # album
+ if not options['classical_work_parts']:
+ if 'composer_lastnames' in self.album_artists[album]:
+ last_names = seq_last_names(self, album)
+ self.append_tag(
+ release_id,
+ tm,
+ '~cea_album_composer_lastnames',
+ last_names)
+ # otherwise this is done in the workparts class, which has all
+ # composer info
+
+ # process tag mapping
+ tm['~cea_artists_complete'] = "Y"
+ map_tags(options, release_id, album, tm)
+
+ # write out options and errors/warnings to tags
+ if options['cea_options_tag'] != "":
+ self.cea_options = collections.defaultdict(
+ lambda: collections.defaultdict(
+ lambda: collections.defaultdict(dict)))
+
+ for opt in plugin_options(
+ 'artists') + plugin_options('tag') + plugin_options('picard'):
+ if 'name' in opt:
+ if 'value' in opt:
+ if options[opt['option']]:
+ self.cea_options['Classical Extras']['Artists options'][opt['name']] = opt['value']
+ else:
+ self.cea_options['Classical Extras']['Artists options'][opt['name']
+ ] = options[opt['option']]
+
+ for opt in plugin_options('tag_detail'):
+ if opt['option'] != "":
+ name_list = opt['name'].split("_")
+ self.cea_options['Classical Extras']['Artists options'][name_list[0]
+ ][name_list[1]] = options[opt['option']]
+
+ if options['ce_version_tag'] and options['ce_version_tag'] != "":
+ self.append_tag(release_id, tm, options['ce_version_tag'], str(
+ 'Version ' + tm['~cea_version'] + ' of Classical Extras'))
+ if options['cea_options_tag'] and options['cea_options_tag'] != "":
+ self.append_tag(
+ release_id,
+ tm,
+ options['cea_options_tag'] +
+ ':artists_options',
+ json.loads(
+ json.dumps(
+ self.cea_options)))
+
+
+ def infer_genres(self, release_id, options, track, tm):
+ """
+ Infer a genre from the artist/instrument metadata
+ :param release_id:
+ :param track:
+ :param tm: track metadata
+ :return:
+ """
+ # Note that this is now mixed in with other sources of genres in def map_tags
+ # ~cea_work_type_if_classical is used for types that are specifically classical
+ # and is only applied in map_tags if the track is deemed to be
+ # classical
+ if (self.globals[track]['is_recording'] and options['classical_work_parts']
+ and '~artists_sort' in tm and 'composersort' in tm
+ and any(x in tm['~artists_sort'] for x in tm['composersort'])
+ and 'writer' not in tm
+ and not any(x in tm['~artists_sort'] for x in tm['~cea_performers_sort'])):
+ self.append_tag(
+ release_id, tm, '~cea_work_type', 'Classical')
+
+ if isinstance(tm['~cea_soloists'], str):
+ soloists = re.split(
+ '|'.join(
+ self.SEPARATORS),
+ tm['~cea_soloists'])
+ else:
+ soloists = tm['~cea_soloists']
+ if '~cea_vocalists' in tm:
+ if isinstance(tm['~cea_vocalists'], str):
+ vocalists = re.split(
+ '|'.join(
+ self.SEPARATORS),
+ tm['~cea_vocalists'])
+ else:
+ vocalists = tm['~cea_vocalists']
+ else:
+ vocalists = []
+
+ if '~cea_ensembles' in tm:
+ large = False
+ if 'performer:orchestra' in tm:
+ large = True
+ self.append_tag(
+ release_id, tm, '~cea_work_type_if_classical', 'Orchestral')
+ if '~cea_soloists' in tm:
+ if 'vocals' in tm['~cea_instruments_all']:
+ self.append_tag(
+ release_id, tm, '~cea_work_type', 'Vocal')
+ if len(soloists) == 1:
+ if soloists != vocalists:
+ self.append_tag(
+ release_id, tm, '~cea_work_type_if_classical', 'Concerto')
+ else:
+ self.append_tag(
+ release_id, tm, '~cea_work_type_if_classical', 'Aria')
+ elif len(soloists) == 2:
+ self.append_tag(
+ release_id, tm, '~cea_work_type_if_classical', 'Duet')
+ if not vocalists:
+ self.append_tag(
+ release_id, tm, '~cea_work_type_if_classical', 'Concerto')
+ elif len(soloists) == 3:
+ self.append_tag(
+ release_id, tm, '~cea_work_type_if_classical', 'Trio')
+ elif len(soloists) == 4:
+ self.append_tag(
+ release_id, tm, '~cea_work_type_if_classical', 'Quartet')
+
+ if 'performer:choir' in tm or 'performer:choir vocals' in tm:
+ large = True
+ self.append_tag(
+ release_id, tm, '~cea_work_type_if_classical', 'Choral')
+ self.append_tag(
+ release_id, tm, '~cea_work_type', 'Vocal')
+ else:
+ if large and 'soloists' in tm and tm['soloists'].count(
+ 'vocals') > 1:
+ self.append_tag(
+ release_id, tm, '~cea_work_type_if_classical', 'Opera')
+ if not large:
+ if '~cea_soloists' not in tm:
+ self.append_tag(
+ release_id, tm, '~cea_work_type_if_classical', 'Chamber music')
+ else:
+ if vocalists:
+ self.append_tag(
+ release_id, tm, '~cea_work_type', 'Song')
+ self.append_tag(
+ release_id, tm, '~cea_work_type', 'Vocal')
+ else:
+ self.append_tag(
+ release_id, tm, '~cea_work_type_if_classical', 'Chamber music')
+ else:
+ if len(soloists) == 1:
+ if vocalists != soloists:
+ self.append_tag(
+ release_id, tm, '~cea_work_type', 'Instrumental')
+ else:
+ self.append_tag(
+ release_id, tm, '~cea_work_type', 'Song')
+ self.append_tag(
+ release_id, tm, '~cea_work_type', 'Vocal')
+ elif len(soloists) == 2:
+ self.append_tag(
+ release_id, tm, '~cea_work_type_if_classical', 'Duet')
+ elif len(soloists) == 3:
+ self.append_tag(
+ release_id, tm, '~cea_work_type_if_classical', 'Trio')
+ elif len(soloists) == 4:
+ self.append_tag(
+ release_id, tm, '~cea_work_type_if_classical', 'Quartet')
+ else:
+ if not vocalists:
+ self.append_tag(
+ release_id, tm, '~cea_work_type_if_classical', 'Chamber music')
+ else:
+ self.append_tag(
+ release_id, tm, '~cea_work_type', 'Song')
+ self.append_tag(
+ release_id, tm, '~cea_work_type', 'Vocal')
+
+
+ def append_tag(self, release_id, tm, tag, source):
+ """
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param tm:
+ :param tag:
+ :param source:
+ :return:
+ """
+ write_log(
+ release_id,
+ 'info',
+ "Extra Artists - appending %s to %s",
+ source,
+ tag)
+ append_tag(release_id, tm, tag, source, self.SEPARATORS)
+
+ def set_performer(self, release_id, album, track, performerList, tm):
+ """
+ Sets the performer-related tags
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param album:
+ :param track:
+ :param performerList: see below
+ :param tm:
+ :return:
+ """
+ # performerList is in format [(artist_type, [instrument list],[name list],[sort_name list],
+ # instrument_sort, type_sort),(.....etc]
+ # Sorted by type_sort then sort name then instrument_sort
+ write_log(release_id, 'debug', "Extra Artists - set_performer")
+ write_log(release_id, 'info', "Performer list is:")
+ write_log(release_id, 'info', performerList)
+ options = self.options[track]
+ # tag strings are a tuple (Picard tag, cea tag, Picard sort tag, cea
+ # sort tag)
+ tag_strings = const.tag_strings('~cea')
+ # insertions lists artist types where names in the main Picard tags may be updated for annotations
+ # (not for performer types as Picard will write performer:inst as Performer name (inst) )
+ insertions = const.INSERTIONS
+
+ # First remove all existing performer tags
+ del_list = []
+ for meta in tm:
+ if 'performer' in meta:
+ del_list.append(meta)
+ for del_item in del_list:
+ del tm[del_item]
+ last_artist = []
+ last_inst_list = []
+ last_instrument = None
+ artist_inst = []
+ artist_inst_list = {}
+ for performer in performerList:
+ artist_type = performer[0]
+ if artist_type not in tag_strings:
+ return None
+ if artist_type in ['instrument', 'vocal', 'performing orchestra']:
+ if performer[1]:
+ inst_list = performer[1]
+ attrib_list = []
+ for attrib in ['solo', 'guest', 'additional']:
+ if attrib in inst_list:
+ inst_list.remove(attrib)
+ attrib_list.append(attrib)
+ attribs = " ".join(attrib_list)
+ instrument = ", ".join(inst_list)
+ if not options['cea_no_solo'] and attrib_list:
+ instrument = attribs + " " + instrument
+ if performer[3] == last_artist:
+ if instrument != last_instrument:
+ artist_inst.append(instrument)
+ else:
+ if inst_list == last_inst_list:
+ write_log(
+ release_id, 'warning', 'Duplicated performer information for %s'
+ ' (may be in Release Relationship as well as Track Relationship).'
+ ' Duplicates have been ignored.', performer[3])
+ if self.WARNING:
+ self.append_tag(
+ release_id,
+ tm,
+ '~cea_warning',
+ '2. Duplicated performer information for "' +
+ '; '.join(
+ performer[3]) +
+ '" (may be in Release Relationship as well as Track Relationship).'
+ ' Duplicates have been ignored.')
+ else:
+ artist_inst = [instrument]
+ last_artist = performer[3]
+ last_inst_list = inst_list
+ last_instrument = instrument
+
+ instrument = ", ".join(artist_inst)
+ else:
+ instrument = None
+ if artist_type == 'performing orchestra':
+ instrument = 'orchestra'
+ artist_inst_list[tuple(performer[3])] = instrument
+ for performer in performerList:
+ artist_type = performer[0]
+ if artist_type not in tag_strings:
+ return None
+ performing_artist = False if artist_type in [
+ 'arranger', 'instrument arranger', 'orchestrator', 'vocal arranger'] else True
+ if True and artist_type in [
+ 'instrument',
+ 'vocal',
+ 'performing orchestra']: # There may be an option here (to replace 'True')
+ # Currently groups instruments by artist - alternative has been
+ # tested if required
+ instrument = artist_inst_list[tuple(performer[3])]
+ else:
+ if performer[1]:
+ inst_list = performer[1]
+ if options['cea_no_solo']:
+ for attrib in ['solo', 'guest', 'additional']:
+ if attrib in inst_list:
+ inst_list.remove(attrib)
+ instrument = " ".join(inst_list)
+ else:
+ instrument = None
+ if artist_type == 'performing orchestra':
+ instrument = 'orchestra'
+ sub_strings = {'instrument': instrument,
+ 'vocal': instrument # ,
+ # 'instrument arranger': instrument,
+ # 'vocal arranger': instrument
+ }
+ for typ in ['concertmaster']:
+ if options['cea_' + typ] and options['cea_arrangers']:
+ sub_strings[typ] = ':' + options['cea_' + typ]
+
+ if options['cea_arranger']:
+ if instrument:
+ arr_inst = options['cea_arranger'] + ' ' + instrument
+ else:
+ arr_inst = options['cea_arranger']
+ else:
+ arr_inst = instrument
+ annotations = {'instrument': instrument,
+ 'vocal': instrument,
+ 'performing orchestra': instrument,
+ 'chorus master': options['cea_chorusmaster'],
+ 'concertmaster': options['cea_concertmaster'],
+ 'arranger': options['cea_arranger'],
+ 'instrument arranger': arr_inst,
+ 'orchestrator': options['cea_orchestrator'],
+ 'vocal arranger': arr_inst}
+ tag = tag_strings[artist_type][0]
+ cea_tag = tag_strings[artist_type][1]
+ sort_tag = tag_strings[artist_type][2]
+ cea_sort_tag = tag_strings[artist_type][3]
+ cea_names_tag = cea_tag[:-1] + '_names'
+ cea_instrumented_tag = cea_names_tag + '_instrumented'
+ if artist_type in sub_strings:
+ if sub_strings[artist_type]:
+ tag += sub_strings[artist_type]
+ else:
+ write_log(
+ release_id,
+ 'warning',
+ 'No instrument/sub-key available for artist_type %s. Performer = %s. Track is %s',
+ artist_type,
+ performer[2],
+ track)
+
+ if tag:
+ if '~ce_tag_cleared_' + \
+ tag not in tm or not tm['~ce_tag_cleared_' + tag] == "Y":
+ if tag in tm:
+ write_log(release_id, 'info', 'delete tag %s', tag)
+ del tm[tag]
+ tm['~ce_tag_cleared_' + tag] = "Y"
+ if sort_tag:
+ if '~ce_tag_cleared_' + \
+ sort_tag not in tm or not tm['~ce_tag_cleared_' + sort_tag] == "Y":
+ if sort_tag in tm:
+ del tm[sort_tag]
+ tm['~ce_tag_cleared_' + sort_tag] = "Y"
+
+ name_list = performer[2]
+ for ind, name in enumerate(name_list):
+ performer_type = ''
+ sort_name = performer[3][ind]
+ no_credit = True
+ # change name to as-credited
+ if (performing_artist and options['cea_performer_credited'] or
+ not performing_artist and options['cea_composer_credited']):
+ if sort_name in self.artist_credits[album]:
+ no_credit = False
+ name = self.artist_credits[album][sort_name]
+ # over-ride with aliases and use standard MB name (not
+ # as-credited) if no alias
+ if (options['cea_aliases'] or not performing_artist and options['cea_aliases_composer']) and (
+ no_credit or options['cea_alias_overrides']):
+ if sort_name in self.artist_aliases:
+ name = self.artist_aliases[sort_name]
+ # fix cyrillic names if not already fixed
+ if options['cea_cyrillic']:
+ if not only_roman_chars(name):
+ name = remove_middle(unsort(sort_name))
+ # Only remove middle name where the existing
+ # performer is in non-latin script
+ annotated_name = name
+ if instrument:
+ instrumented_name = name + ' (' + instrument + ')'
+ else:
+ instrumented_name = name
+ # add annotations and write performer tags
+ if artist_type in annotations:
+ if annotations[artist_type]:
+ annotated_name += ' (' + annotations[artist_type] + ')'
+ else:
+ write_log(
+ release_id,
+ 'warning',
+ 'No annotation (instrument) available for artist_type %s.'
+ ' Performer = %s. Track is %s',
+ artist_type,
+ performer[2],
+ track)
+ if artist_type in insertions and options['cea_arrangers']:
+ self.append_tag(release_id, tm, tag, annotated_name)
+ else:
+ if options['cea_arrangers'] or artist_type == tag:
+ self.append_tag(release_id, tm, tag, name)
+
+ if options['cea_arrangers'] or artist_type == tag:
+ if sort_tag:
+ self.append_tag(release_id, tm, sort_tag, sort_name)
+ if options['cea_tag_sort'] and '~' in sort_tag:
+ explicit_sort_tag = sort_tag.replace('~', '')
+ self.append_tag(
+ release_id, tm, explicit_sort_tag, sort_name)
+
+ self.append_tag(release_id, tm, cea_tag, annotated_name)
+ self.append_tag(release_id, tm, cea_names_tag, name)
+ if instrumented_name != name:
+ self.append_tag(
+ release_id,
+ tm,
+ cea_instrumented_tag,
+ instrumented_name)
+
+ if cea_sort_tag:
+ self.append_tag(release_id, tm, cea_sort_tag, sort_name)
+
+ # differentiate soloists etc and write related tags
+ if artist_type == 'performing orchestra' or (
+ instrument and instrument in self.ENSEMBLE_TYPES) or self.ensemble_type(name):
+ performer_type = 'ensembles'
+ self.append_tag(
+ release_id, tm, '~cea_ensembles', instrumented_name)
+ self.append_tag(
+ release_id, tm, '~cea_ensemble_names', name)
+ self.append_tag(
+ release_id, tm, '~cea_ensembles_sort', sort_name)
+ elif artist_type in ['performer', 'instrument', 'vocal']:
+ performer_type = 'soloists'
+ self.append_tag(
+ release_id, tm, '~cea_soloists', instrumented_name)
+ self.append_tag(release_id, tm, '~cea_soloist_names', name)
+ self.append_tag(
+ release_id, tm, '~cea_soloists_sort', sort_name)
+ if artist_type == "vocal":
+ self.append_tag(
+ release_id, tm, '~cea_vocalists', instrumented_name)
+ self.append_tag(
+ release_id, tm, '~cea_vocalist_names', name)
+ self.append_tag(
+ release_id, tm, '~cea_vocalists_sort', sort_name)
+ elif instrument:
+ self.append_tag(
+ release_id, tm, '~cea_instrumentalists', instrumented_name)
+ self.append_tag(
+ release_id, tm, '~cea_instrumentalist_names', name)
+ self.append_tag(
+ release_id, tm, '~cea_instrumentalists_sort', sort_name)
+ else:
+ self.append_tag(
+ release_id, tm, '~cea_other_soloists', instrumented_name)
+ self.append_tag(
+ release_id, tm, '~cea_other_soloist_names', name)
+ self.append_tag(
+ release_id, tm, '~cea_other_soloists_sort', sort_name)
+
+ # set album artists
+ if performer_type or artist_type == 'conductor':
+ cea_album_tag = cea_tag.replace(
+ 'cea', 'cea_album').replace(
+ 'performers', performer_type)
+ cea_album_sort_tag = cea_sort_tag.replace(
+ 'cea', 'cea_album').replace(
+ 'performers', performer_type)
+ if stripsir(name) in tm['~albumartists'] or stripsir(
+ sort_name) in tm['~albumartists_sort']:
+ self.append_tag(release_id, tm, cea_album_tag, name)
+ self.append_tag(
+ release_id, tm, cea_album_sort_tag, sort_name)
+ else:
+ if performer_type:
+ self.append_tag(
+ release_id, tm, '~cea_support_performers', instrumented_name)
+ self.append_tag(
+ release_id, tm, '~cea_support_performer_names', name)
+ self.append_tag(
+ release_id, tm, '~cea_support_performers_sort', sort_name)
+
+##############
+##############
+# WORK PARTS #
+##############
+##############
+
+
+class PartLevels():
+ # QUEUE-HANDLING
+ class WorksQueue(LockableObject):
+ """Object for managing the queue of lookups"""
+
+ def __init__(self):
+ LockableObject.__init__(self)
+ self.queue = {}
+
+ def __contains__(self, name):
+ return name in self.queue
+
+ def __iter__(self):
+ return self.queue.__iter__()
+
+ def __getitem__(self, name):
+ self.lock_for_read()
+ value = self.queue[name] if name in self.queue else None
+ self.unlock()
+ return value
+
+ def __setitem__(self, name, value):
+ self.lock_for_write()
+ self.queue[name] = value
+ self.unlock()
+
+ def append(self, name, value):
+ self.lock_for_write()
+ if name in self.queue:
+ self.queue[name].append(value)
+ value = False
+ else:
+ self.queue[name] = [value]
+ value = True
+ self.unlock()
+ return value
+
+ def remove(self, name):
+ self.lock_for_write()
+ value = None
+ if name in self.queue:
+ value = self.queue[name]
+ del self.queue[name]
+ self.unlock()
+ return value
+
+ # INITIALISATION
+
+ def __init__(self):
+ self.works_cache = {}
+ # maintains list of parent of each workid, or None if no parent found,
+ # so that XML lookup need only executed if no existing record
+
+ self.partof = collections.defaultdict(dict)
+ # the inverse of the above (immediate children of each parent)
+ # but note that this is specific to the album as children may vary between albums
+ # so format is {album1{parent1: child1, parent2:, child2},
+ # album2{....}}
+
+ self.works_queue = self.WorksQueue()
+ # lookup queue - holds track/album pairs for each queued workid (may be
+ # more than one pair per id, especially for higher-level parts)
+
+ self.parts = collections.defaultdict(
+ lambda: collections.defaultdict(dict))
+ # metadata collection for all parts - structure is {workid: {name: ,
+ # parent: , (track,album): {part_levels}}, etc}
+
+ self.top_works = collections.defaultdict(dict)
+ # metadata collection for top-level works for (track, album) -
+ # structure is {(track, album): {workId: }, etc}
+
+ self.trackback = collections.defaultdict(
+ lambda: collections.defaultdict(dict))
+ # hierarchical iterative work structure - {album: {id: , children:{id:
+ # , children{}, id: etc}, id: etc} }
+
+ self.child_listing = collections.defaultdict(list)
+ # contains list of workIds which are descendants of a given workId, to
+ # prevent recursion when adding new ids
+
+ self.work_listing = collections.defaultdict(list)
+ # contains list of workIds for each album
+
+ self.top = collections.defaultdict(list)
+ # self.top[album] = list of work Ids which are top-level works in album
+
+ self.options = collections.defaultdict(dict)
+ # active Classical Extras options for current track
+
+ self.synonyms = collections.defaultdict(dict)
+ # active synonym options for current track
+
+ self.replacements = collections.defaultdict(dict)
+ # active synonym options for current track
+
+ self.file_works = collections.defaultdict(list)
+ # list of works derived from SongKong-style file tags
+ # structure is {(album, track): [{workid: , name: }, {workid: ....}}
+
+ self.album_artists = collections.defaultdict(
+ lambda: collections.defaultdict(dict))
+ # collection of artists to be applied at album level
+
+ self.artist_aliases = {}
+ # collection of alias names - format is {sort_name: alias_name, ...}
+
+ self.artist_credits = collections.defaultdict(dict)
+ # collection of credited-as names - format is {album: {sort_name: credit_name,
+ # ...}, ...}
+
+ self.release_artists_sort = collections.defaultdict(list)
+ # collection of release artists - format is {album: [sort_name_1,
+ # sort_name_2, ...]}
+
+ self.lyricist_filled = collections.defaultdict(dict)
+ # Boolean for each track to indicate if lyricist has been found (don't
+ # want to add more from higher levels)
+
+ self.orphan_tracks = collections.defaultdict(list)
+ # To keep a list for each album of tracks which do not have works -
+ # format is {album: [track1, track2, ...], etc}
+
+ self.tracks = collections.defaultdict(
+ lambda: collections.defaultdict(dict))
+ # To keep a list of all tracks for the album - format is {album:
+ # {track1: {movement-group: movementgroup, movement-number: movementnumber},
+ # track2: {}, ..., etc}, album2: etc}
+
+ ########################################
+ # SECTION 1 - Initial track processing #
+ ########################################
+
+ def add_work_info(
+ self,
+ album,
+ track_metadata,
+ trackXmlNode,
+ releaseXmlNode):
+ """
+ Main Routine - run for each track
+ :param album:
+ :param track_metadata:
+ :param trackXmlNode:
+ :param releaseXmlNode:
+ :return:
+ """
+ release_id = track_metadata['musicbrainz_albumid']
+ if 'start' not in release_status[release_id]:
+ release_status[release_id]['start'] = datetime.now()
+ if 'lookups' not in release_status[release_id]:
+ release_status[release_id]['lookups'] = 0
+ release_status[release_id]['name'] = track_metadata['album']
+ release_status[release_id]['works'] = True
+ if config.setting['log_debug'] or config.setting['log_info']:
+ write_log(
+ release_id,
+ 'debug',
+ 'STARTING WORKS PROCESSING FOR ALBUM %s, DISC %s, TRACK %s',
+ track_metadata['album'],
+ track_metadata['discnumber'],
+ track_metadata['tracknumber'] +
+ ' ' +
+ track_metadata['title'])
+ # clear the cache if required (if this is not done, then queue count may get out of sync)
+ # Jump through hoops to get track object!!
+ track = album._new_tracks[-1]
+ tm = track.metadata
+ if config.setting['log_debug'] or config.setting['log_info']:
+ write_log(
+ release_id,
+ 'debug',
+ 'Cache setting for track %s is %s',
+ track,
+ config.setting['use_cache'])
+
+ # OPTIONS - OVER-RIDE IF REQUIRED
+ if '~ce_options' not in tm:
+ if config.setting['log_debug'] or config.setting['log_info']:
+ write_log(release_id, 'debug', 'Workparts gets track first...')
+ get_options(release_id, album, track)
+ options = interpret(tm['~ce_options'])
+
+ if not options:
+ if config.setting['log_error']:
+ write_log(
+ release_id,
+ 'error',
+ 'Work Parts. Failure to read saved options for track %s. options = %s',
+ track,
+ tm['~ce_options'])
+ options = option_settings(config.setting)
+ self.options[track] = options
+
+ # CONSTANTS
+ write_log(release_id, 'basic', 'Options: %s' ,options)
+ self.ERROR = options["log_error"]
+ self.WARNING = options["log_warning"]
+ self.SEPARATORS = ['; ']
+ self.EQ = "EQ_TO_BE_REVERSED" # phrase to indicate that a synonym has been used
+
+ self.get_sk_tags(release_id, album, track, tm, options)
+ self.synonyms[track] = self.get_text_tuples(
+ release_id, track, 'synonyms') # a list of tuples
+ self.replacements[track] = self.get_text_tuples(
+ release_id, track, 'replacements') # a list of tuples
+
+ # Continue?
+ if not options["classical_work_parts"]:
+ return
+
+ # OPTION-DEPENDENT CONSTANTS:
+ # Maximum number of XML- lookup retries if error returned from server
+ self.MAX_RETRIES = options["cwp_retries"]
+ self.USE_CACHE = options["use_cache"]
+ if options["cwp_partial"] and options["cwp_partial_text"] and options["cwp_level0_works"]:
+ options["cwp_removewords_p"] = options["cwp_removewords"] + \
+ ", " + options["cwp_partial_text"] + ' '
+ else:
+ options["cwp_removewords_p"] = options["cwp_removewords"]
+ # Explanation:
+ # If "Partial" is selected then the level 0 work name will have PARTIAL_TEXT appended to it.
+ # If a recording is split across several tracks then each sub-part (quasi-movement) will have the same name
+ # (with the PARTIAL_TEXT added). If level 0 is used to source work names then the level 1 work name will be
+ # changed to be this repeated name and will therefore also include PARTIAL_TEXT.
+ # So we need to add PARTIAL_TEXT to the prefixes list to ensure it is excluded from the level 1 work name.
+ #
+ write_log(
+ release_id,
+ 'debug',
+ "PartLevels - LOAD NEW TRACK: :%s",
+ track)
+ # write_log(release_id, 'info', "trackXmlNode:") # warning - may break Picard
+
+ # first time for this album (reloads each refresh)
+ if tm['discnumber'] == '1' and tm['tracknumber'] == '1':
+ # get artist aliases - these are cached so can be re-used across
+ # releases, but are reloaded with each refresh
+ get_aliases(self, release_id, album, options, releaseXmlNode)
+
+ # fix titles which include composer name
+ composersort =[]
+ if 'compposersort' in tm:
+ composersort = str_to_list(['composersort'])
+ composerlastnames = []
+ for composer in composersort:
+ lname = re.compile(r'(.*),')
+ match = lname.search(composer)
+ if match:
+ composerlastnames.append(match.group(1))
+ else:
+ composerlastnames.append(composer)
+ title = track_metadata['title']
+ colons = title.count(":")
+ if colons > 0:
+ title_split = title.split(': ', 1)
+ test = title_split[0]
+ if test in composerlastnames:
+ track_metadata['~cwp_title'] = title_split[1]
+
+ # now process works
+ write_log(
+ release_id,
+ 'info',
+ 'PartLevels - add_work_info - metadata load = %r',
+ track_metadata)
+ workIds = []
+ if 'musicbrainz_workid' in tm:
+ workIds = str_to_list(tm['musicbrainz_workid'])
+ if workIds and not (options["ce_no_run"] and (
+ not tm['~ce_file'] or tm['~ce_file'] == "None")):
+ self.build_work_info(release_id, options, trackXmlNode, album, track, track_metadata, workIds)
+
+ else: # no work relation
+ write_log(
+ release_id,
+ 'warning',
+ "WARNING - no works for this track: \"%s\"",
+ title)
+ self.append_tag(
+ release_id,
+ track_metadata,
+ '~cwp_warning',
+ '3. No works for this track')
+ if album in self.orphan_tracks:
+ if track not in self.orphan_tracks[album]:
+ self.orphan_tracks[album].append(track)
+ else:
+ self.orphan_tracks[album] = [track]
+ # Don't publish metadata yet until all album is processed
+
+ # last track
+ write_log(
+ release_id,
+ 'debug',
+ 'Check for last track. Requests = %s, Tracknumber = %s, Totaltracks = %s,'
+ ' Discnumber = %s, Totaldiscs = %s',
+ album._requests,
+ track_metadata['tracknumber'],
+ track_metadata['totaltracks'],
+ track_metadata['discnumber'],
+ track_metadata['totaldiscs'])
+ if album._requests == 0 and track_metadata['tracknumber'] == track_metadata[
+ 'totaltracks'] and track_metadata['discnumber'] == track_metadata['totaldiscs']:
+ self.process_album(release_id, album)
+ release_status[release_id]['works-done'] = datetime.now()
+ close_log(release_id, 'works')
+
+
+ def build_work_info(self, release_id, options, trackXmlNode, album, track, track_metadata, workIds):
+ """
+ Construct the work metadata, taking into account partial recordings and medleys
+ :param release_id:
+ :param options:
+ :param trackXmlNode: JSON returned by the webservice
+ :param album:
+ :param track:
+ :param track_metadata:
+ :param workIds: work ids for this track
+ :return:
+ """
+ work_list_info = []
+ keyed_workIds = {}
+ for i, workId in enumerate(workIds):
+
+ # sort by ordering_key, if any
+ match_tree = [
+ 'recording',
+ 'relations',
+ 'target-type:work',
+ 'work',
+ 'id:' + workId]
+ rels = parse_data(release_id, trackXmlNode, [], *match_tree)
+ # for recordings which are ordered within track:-
+ match_tree_1 = [
+ 'ordering-key']
+ # for recordings of works which are ordered as part of parent
+ # (may be duplicated by top-down check later):-
+ match_tree_2 = [
+ 'relations',
+ 'target-type:work',
+ 'type:parts',
+ 'direction:backward',
+ 'ordering-key']
+ parse_result = parse_data(release_id,
+ rels,
+ [],
+ *match_tree_1) + parse_data(release_id,
+ rels,
+ [],
+ *match_tree_2)
+ write_log(
+ release_id,
+ 'info',
+ 'multi-works - ordering key: %s',
+ parse_result)
+ if parse_result:
+ if isinstance(parse_result[0], int):
+ key = parse_result[0]
+ elif isinstance(parse_result[0], str) and parse_result[0].isdigit():
+ key = int(parse_result[0])
+ else:
+ key = 100 + i
+ else:
+ key = 100 + i
+ keyed_workIds[key] = workId
+ partial = False
+ for key in sorted(keyed_workIds):
+ workId = keyed_workIds[key]
+ work_rels = parse_data(
+ release_id,
+ trackXmlNode,
+ [],
+ 'recording',
+ 'relations',
+ 'target-type:work',
+ 'work.id:' + workId)
+ write_log(release_id, 'info', 'work_rels: %s', work_rels)
+ work_attributes = parse_data(
+ release_id, work_rels, [], 'attributes')[0]
+ write_log(
+ release_id,
+ 'info',
+ 'work_attributes: %s',
+ work_attributes)
+ work_titles = parse_data(
+ release_id, work_rels, [], 'work', 'title')
+ work_list_info_item = {
+ 'id': workId,
+ 'attributes': work_attributes,
+ 'titles': work_titles}
+ work_list_info.append(work_list_info_item)
+ work = []
+ for title in work_titles:
+ work.append(title)
+ if options['cwp_partial']:
+ # treat the recording as work level 0 and the work of which it
+ # is a partial recording as work level 1
+ if 'partial' in work_attributes:
+ partial = True
+ parentId = workId
+ workId = track_metadata['musicbrainz_recordingid']
+
+ works = []
+ for w in work:
+ partwork = w
+ works.append(partwork)
+
+ write_log(
+ release_id,
+ 'info',
+ "Id %s is PARTIAL RECORDING OF id: %s, name: %s",
+ workId,
+ parentId,
+ work)
+ work_list_info_item = {
+ 'id': workId,
+ 'attributes': [],
+ 'titles': works,
+ 'parent': parentId}
+ work_list_info.append(work_list_info_item)
+ write_log(
+ release_id,
+ 'info',
+ 'work_list_info: %s',
+ work_list_info)
+ # we now have a list of items, where the id of each is a work id for the track or
+ # (multiple instances of) the recording id (for partial works)
+ # we need to turn this into a usable hierarchy - i.e. just one item
+ workId_list = []
+ work_list = []
+ parent_list = []
+ attribute_list = []
+ workId_list_p = []
+ work_list_p = []
+ attribute_list_p = []
+ for w in work_list_info:
+ if 'partial' not in w['attributes'] or not options[
+ 'cwp_partial']: # just do the bottom-level 'works' first
+ workId_list.append(w['id'])
+ work_list += w['titles']
+ attribute_list += w['attributes']
+ if 'parent' in w:
+ if w['parent'] not in parent_list: # avoid duplicating parents!
+ parent_list.append(w['parent'])
+ else:
+ workId_list_p.append(w['id'])
+ work_list_p += w['titles']
+ attribute_list_p += w['attributes']
+ # de-duplicate work names
+ # list(set()) won't work as need to retain order
+ work_list = list(collections.OrderedDict.fromkeys(work_list))
+ work_list_p = list(collections.OrderedDict.fromkeys(work_list_p))
+
+ workId_tuple = tuple(workId_list)
+ workId_tuple_p = tuple(workId_list_p)
+ if workId_tuple not in self.work_listing[album]:
+ self.work_listing[album].append(workId_tuple)
+ if workId_tuple not in self.parts or not self.USE_CACHE:
+ self.parts[workId_tuple]['name'] = work_list
+ if parent_list:
+ if workId_tuple in self.works_cache:
+ self.works_cache[workId_tuple] += parent_list
+ self.parts[workId_tuple]['parent'] += parent_list
+ else:
+ self.works_cache[workId_tuple] = parent_list
+ self.parts[workId_tuple]['parent'] = parent_list
+ self.parts[workId_tuple_p]['name'] = work_list_p
+ if workId_tuple_p not in self.work_listing[album]:
+ self.work_listing[album].append(workId_tuple_p)
+
+ if 'medley' in attribute_list_p:
+ self.parts[workId_tuple_p]['medley'] = True
+
+ if 'medley' in attribute_list:
+ self.parts[workId_tuple]['medley'] = True
+
+ if partial:
+ self.parts[workId_tuple]['partial'] = True
+
+ self.trackback[album][workId_tuple]['id'] = workId_list
+ if 'meta' in self.trackback[album][workId_tuple]:
+ if (track,
+ album) not in self.trackback[album][workId_tuple]['meta']:
+ self.trackback[album][workId_tuple]['meta'].append(
+ (track, album))
+ else:
+ self.trackback[album][workId_tuple]['meta'] = [(track, album)]
+ write_log(
+ release_id,
+ 'info',
+ "Trackback for %s is %s. Partial = %s",
+ track,
+ self.trackback[album][workId_tuple],
+ partial)
+
+ if workId_tuple in self.works_cache and (
+ self.USE_CACHE or partial):
+ write_log(
+ release_id,
+ 'debug',
+ "GETTING WORK METADATA FROM CACHE, for work %s",
+ workId_tuple)
+ if workId_tuple not in self.work_listing[album]:
+ self.work_listing[album].append(workId_tuple)
+ not_in_cache = self.check_cache(
+ track_metadata, album, track, workId_tuple, [])
+ else:
+ if partial:
+ not_in_cache = [workId_tuple_p]
+ else:
+ not_in_cache = [workId_tuple]
+ for workId_tuple in not_in_cache:
+ if not self.USE_CACHE:
+ if workId_tuple in self.works_cache:
+ del self.works_cache[workId_tuple]
+ self.work_not_in_cache(release_id, album, track, workId_tuple)
+
+
+ def get_sk_tags(self, release_id, album, track, tm, options):
+ """
+ Get file tags which are consistent with SongKong's metadata usage
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param album:
+ :param track:
+ :param tm:
+ :param options:
+ :return:
+ """
+ if options["cwp_use_sk"]:
+ if '~ce_file' in tm and interpret(tm['~ce_file']):
+ music_file = tm['~ce_file']
+ orig_metadata = album.tagger.files[music_file].orig_metadata
+ if 'musicbrainz_work_composition_id' in orig_metadata and 'musicbrainz_workid' in orig_metadata:
+ if 'musicbrainz_work_composition' in orig_metadata:
+ if 'musicbrainz_work' in orig_metadata:
+ if orig_metadata['musicbrainz_work_composition_id'] == orig_metadata[
+ 'musicbrainz_workid'] \
+ and orig_metadata['musicbrainz_work_composition'] != orig_metadata[
+ 'musicbrainz_work']:
+ # Picard may have overwritten SongKong tag (top
+ # work id) with bottom work id
+ write_log(
+ release_id,
+ 'warning',
+ 'File tag musicbrainz_workid incorrect? id = %s. Sourcing from MB',
+ orig_metadata['musicbrainz_workid'])
+ if self.WARNING:
+ self.append_tag(
+ release_id,
+ tm,
+ '~cwp_warning',
+ '4. File tag musicbrainz_workid incorrect? id = ' +
+ orig_metadata['musicbrainz_workid'] +
+ '. Sourcing from MB')
+ return None
+ write_log(
+ release_id,
+ 'info',
+ 'Read from file tag: musicbrainz_work_composition_id: %s',
+ orig_metadata['musicbrainz_work_composition_id'])
+ self.file_works[(album, track)].append({
+ 'workid': orig_metadata['musicbrainz_work_composition_id'].split('; '),
+ 'name': orig_metadata['musicbrainz_work_composition']})
+ else:
+ wid = orig_metadata['musicbrainz_work_composition_id']
+ write_log(
+ release_id,
+ 'error',
+ "No matching work name for id tag %s",
+ wid)
+ if self.ERROR:
+ self.append_tag(
+ release_id,
+ tm,
+ '~cwp_error',
+ '2. No matching work name for id tag ' +
+ wid)
+ return None
+ n = 1
+ while 'musicbrainz_work_part_level' + \
+ str(n) + '_id' in orig_metadata:
+ if 'musicbrainz_work_part_level' + \
+ str(n) in orig_metadata:
+ self.file_works[(album, track)].append({
+ 'workid': orig_metadata[
+ 'musicbrainz_work_part_level' + str(n) + '_id'].split('; '),
+ 'name': orig_metadata['musicbrainz_work_part_level' + str(n)]})
+ n += 1
+ else:
+ wid = orig_metadata['musicbrainz_work_part_level' +
+ str(n) + '_id']
+ write_log(
+ release_id, 'error', "No matching work name for id tag %s", wid)
+ if self.ERROR:
+ self.append_tag(
+ release_id,
+ tm,
+ '~cwp_error',
+ '2. No matching work name for id tag ' +
+ wid)
+ break
+ if orig_metadata['musicbrainz_work_composition_id'] != orig_metadata[
+ 'musicbrainz_workid']:
+ if 'musicbrainz_work' in orig_metadata:
+ self.file_works[(album, track)].append({
+ 'workid': orig_metadata['musicbrainz_workid'].split('; '),
+ 'name': orig_metadata['musicbrainz_work']})
+ else:
+ wid = orig_metadata['musicbrainz_workid']
+ write_log(
+ release_id, 'error', "No matching work name for id tag %s", wid)
+ if self.ERROR:
+ self.append_tag(
+ release_id,
+ tm,
+ '~cwp_error',
+ '2. No matching work name for id tag ' +
+ wid)
+ return None
+ file_work_levels = len(self.file_works[(album, track)])
+ write_log(release_id,
+ 'debug',
+ 'Loaded works from file tags for track %s. Works: %s: ',
+ track,
+ self.file_works[(album,
+ track)])
+ for i, work in enumerate(self.file_works[(album, track)]):
+ workId = tuple(work['workid'])
+ if workId not in self.works_cache: # Use cache in preference to file tags
+ if workId not in self.work_listing[album]:
+ self.work_listing[album].append(workId)
+ self.parts[workId]['name'] = [work['name']]
+ parentId = None
+ parent = ''
+ if i < file_work_levels - 1:
+ parentId = self.file_works[(
+ album, track)][i + 1]['workid']
+ parent = self.file_works[(
+ album, track)][i + 1]['name']
+
+ if parentId:
+ self.works_cache[workId] = parentId
+ self.parts[workId]['parent'] = parentId
+ self.parts[tuple(parentId)]['name'] = [parent]
+ else:
+ # so we remember we looked it up and found none
+ self.parts[workId]['no_parent'] = True
+ self.top_works[(track, album)
+ ]['workId'] = workId
+ if workId not in self.top[album]:
+ self.top[album].append(workId)
+
+ def check_cache(self, tm, album, track, workId_tuple, not_in_cache):
+ """
+ Recursive loop to get cached works
+ :param tm:
+ :param album:
+ :param track:
+ :param workId_tuple:
+ :param not_in_cache:
+ :return:
+ """
+ parentId_tuple = tuple(self.works_cache[workId_tuple])
+ if parentId_tuple not in self.work_listing[album]:
+ self.work_listing[album].append(parentId_tuple)
+
+ if parentId_tuple in self.works_cache:
+ self.check_cache(tm, album, track, parentId_tuple, not_in_cache)
+ else:
+ not_in_cache.append(parentId_tuple)
+ return not_in_cache
+
+ def work_not_in_cache(self, release_id, album, track, workId_tuple):
+ """
+ Determine actions if work not in cache (is it the top or do we need to look up?)
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param album:
+ :param track:
+ :param workId_tuple:
+ :return:
+ """
+
+ write_log(
+ release_id,
+ 'debug',
+ 'Processing work_not_in_cache for workId %s',
+ workId_tuple)
+ ## NB the first condition below is to prevent the side effect of assigning a dictionary entry in self.parts for workId with no details
+ if workId_tuple in self.parts and 'no_parent' in self.parts[workId_tuple] and (
+ self.USE_CACHE or self.options[track]["cwp_use_sk"]) and self.parts[workId_tuple]['no_parent']:
+ write_log(release_id, 'info', '%s is top work', workId_tuple)
+ self.top_works[(track, album)]['workId'] = workId_tuple
+ if album in self.top:
+ if workId_tuple not in self.top[album]:
+ self.top[album].append(workId_tuple)
+ else:
+ self.top[album] = [workId_tuple]
+ else:
+ write_log(
+ release_id,
+ 'info',
+ 'Calling work_add_track to look up parents for %s',
+ workId_tuple)
+ for workId in workId_tuple:
+ self.work_add_track(album, track, workId, 0)
+
+ write_log(
+ release_id,
+ 'debug',
+ 'End of work_not_in_cache for workId %s',
+ workId_tuple)
+
+ def work_add_track(self, album, track, workId, tries, user_data=True):
+ """
+ Add the work to the lookup queue
+ :param user_data:
+ :param album:
+ :param track:
+ :param workId:
+ :param tries: number of lookup attempts
+ :return:
+ """
+ release_id = track.metadata['musicbrainz_albumid']
+ write_log(
+ release_id,
+ 'debug',
+ "ADDING WORK TO LOOKUP QUEUE for work %s",
+ workId)
+ self.album_add_request(release_id, album)
+ # to change the _requests variable to indicate that there are pending
+ # requests for this item and delay Picard from finalizing the album
+ write_log(
+ release_id,
+ 'debug',
+ "Added lookup request for id %s. Requests = %s",
+ workId,
+ album._requests)
+ if self.works_queue.append(
+ workId,
+ (track,
+ album)): # All work combos are queued, but only new workIds are passed to XML lookup
+ host = config.setting["server_host"]
+ port = config.setting["server_port"]
+ path = "/ws/2/%s/%s" % ('work', workId)
+ if config.setting['cwp_aliases'] and config.setting['cwp_aliases_tag_text']:
+ if config.setting['cwp_aliases_tags_user'] and user_data:
+ login = True
+ tag_type = '+tags +user-tags'
+ else:
+ login = False
+ tag_type = '+tags'
+ else:
+ login = False
+ tag_type = ''
+ queryargs = {
+ "inc": "work-rels+artist-rels+label-rels+place-rels+aliases" +
+ tag_type}
+ write_log(
+ release_id,
+ 'debug',
+ "Initiating XML lookup for %s......",
+ workId)
+ if release_id in release_status and 'lookups' in release_status[release_id]:
+ release_status[release_id]['lookups'] += 1
+ return album.tagger.webservice.get(
+ host,
+ port,
+ path,
+ partial(
+ self.work_process,
+ workId,
+ tries),
+ # parse_response_type="xml",
+ priority=True,
+ important=False,
+ mblogin=login,
+ queryargs=queryargs)
+ else:
+ write_log(
+ release_id,
+ 'debug',
+ "Work is already in queue: %s",
+ workId)
+
+ ##########################################################################
+ # SECTION 2 - Works processing #
+ # NB These functions may operate asynchronously over multiple albums (as well as multiple tracks) #
+ ##########################################################################
+
+ def work_process(self, workId, tries, response, reply, error):
+ """
+ Top routine to process the XML/JSON node response from the lookup
+ NB This function may operate over multiple albums (as well as multiple tracks)
+ :param workId:
+ :param tries:
+ :param response:
+ :param reply:
+ :param error:
+ :return:
+ """
+
+ if error:
+ tuples = self.works_queue.remove(workId)
+ for track, album in tuples:
+ release_id = track.metadata['musicbrainz_albumid']
+ write_log(
+ release_id,
+ 'warning',
+ "%r: Network error retrieving work record. Error code %r",
+ workId,
+ error)
+ write_log(
+ release_id,
+ 'debug',
+ "Removed request after network error. Requests = %s",
+ album._requests)
+ if tries < self.MAX_RETRIES:
+ user_data = True
+ write_log(release_id, 'debug', "REQUEUEING...")
+ if str(error) == '204': # Authentication error
+ write_log(
+ release_id, 'debug', "... without user authentication")
+ user_data = False
+ self.append_tag(
+ release_id,
+ track.metadata,
+ '~cwp_error',
+ '3. Authentication failure - data retrieval omits user-specific requests')
+ self.work_add_track(
+ album, track, workId, tries + 1, user_data)
+ else:
+ write_log(
+ release_id,
+ 'error',
+ "EXHAUSTED MAX RE-TRIES for XML lookup for track %s",
+ track)
+ if self.ERROR:
+ self.append_tag(
+ release_id,
+ track.metadata,
+ '~cwp_error',
+ "4. ERROR: MISSING METADATA due to network errors. Re-try or fix manually.")
+ self.album_remove_request(release_id, album)
+ return
+
+ tuples = self.works_queue.remove(workId)
+ if tuples:
+ new_queue = []
+ prev_album = None
+ for (track, album) in tuples:
+ release_id = track.metadata['musicbrainz_albumid']
+ # Note that this need to be set here as the work may cover
+ # multiple albums
+ if album != prev_album:
+ write_log(release_id, 'debug',
+ "Work_process. FOUND WORK: %s for album %s",
+ workId, album)
+ write_log(
+ release_id,
+ 'debug',
+ "Requests for album %s = %s",
+ album,
+ album._requests)
+ prev_album = album
+ write_log(release_id, 'info', "RESPONSE = %s", response)
+ # find the id_tuple(s) key with workId in it
+ wid_list = []
+ for w in self.work_listing[album]:
+ if workId in w and w not in wid_list:
+ wid_list.append(w)
+ write_log(
+ release_id,
+ 'info',
+ 'wid_list for %s is %s',
+ workId,
+ wid_list)
+ for wid in wid_list: # wid is a tuple
+ write_log(
+ release_id,
+ 'info',
+ 'processing workId tuple: %r',
+ wid)
+ metaList = self.work_process_metadata(
+ release_id, workId, wid, track, response)
+ parentList = metaList[0]
+ # returns [[parent id], [parent name], attribute_list] or None if no parent
+ # found
+ arrangers = metaList[1]
+ # not just arrangers - also composers, lyricists etc.
+ if wid in self.parts:
+
+ if arrangers:
+ if 'arrangers' in self.parts[wid]:
+ self.parts[wid]['arrangers'] += arrangers
+ else:
+ self.parts[wid]['arrangers'] = arrangers
+
+ if parentList:
+ # first fix the sort order of multi-works at the prev level
+ # so that recordings of multiple movements of the same parent work will have the
+ # movements listed in the correct order (i.e.
+ # ordering-key, if available)
+ if len(wid) > 1:
+ for idx in wid:
+ if idx == workId:
+ match_tree = [
+ 'relations',
+ 'target-type:work',
+ 'direction:backward',
+ 'ordering-key']
+ parse_result = parse_data(
+ release_id, response, [], *match_tree)
+ write_log(
+ release_id,
+ 'info',
+ 'multi-works - ordering key for id %s is %s',
+ idx,
+ parse_result)
+ if parse_result:
+ if isinstance(
+ parse_result[0], str) and parse_result[0].isdigit():
+ key = int(parse_result[0])
+ elif isinstance(parse_result[0], int):
+ key = parse_result[0]
+ else:
+ key = 9999
+ self.parts[wid]['order'][idx] = key
+
+ parentIds = parentList[0]
+ parents = parentList[1]
+ parent_attributes = parentList[2]
+ write_log(
+ release_id,
+ 'info',
+ 'Parents - ids: %s, names: %s',
+ parentIds,
+ parents)
+ # remove any parents that are descendants of wid as
+ # they will result in circular references
+ del_list = []
+ for i, parentId in enumerate(parentIds):
+ for work_item in wid:
+ if work_item in self.child_listing and parentId in self.child_listing[
+ work_item]:
+ del_list.append(i)
+ for i in list(set(del_list)):
+ removed_id = parentIds.pop(i)
+ removed_name = parents.pop(i)
+ write_log(
+ release_id, 'error', "Found parent which is descendant of child - "
+ "not using, to prevent circular references. id = %s,"
+ " name = %s", removed_id, removed_name)
+ tm = track.metadata
+ self.append_tag(
+ release_id,
+ tm,
+ '~cwp_error',
+ '5. Found parent which which is descendant of child - not using '
+ 'to prevent circular references. id = ' +
+ removed_id +
+ ', name = ' +
+ removed_name)
+ is_collection = False
+ for attribute in parent_attributes:
+ if attribute['collection']:
+ is_collection = True
+ break
+ # de-dup parent ids before we start
+ parentIds = list(
+ collections.OrderedDict.fromkeys(parentIds))
+
+ # add descendants to checklist to prevent recursion
+ for p in parentIds:
+ for w in wid:
+ self.child_listing[p].append(w)
+ if w in self.child_listing:
+ self.child_listing[p] += self.child_listing[w]
+
+ if parentIds:
+ if wid in self.works_cache:
+ # Make sure we haven't done this
+ # relationship before, perhaps for another
+ # album
+
+ if not (set(
+ self.works_cache[wid]) >= set(parentIds)):
+ prev_ids = tuple(self.works_cache[wid])
+ prev_name = self.parts[prev_ids]['name']
+ self.works_cache[wid] = add_list_uniquely(
+ self.works_cache[wid], parentIds)
+ self.parts[wid]['parent'] = add_list_uniquely(
+ self.parts[wid]['parent'], parentIds)
+ index = self.work_listing[album].index(
+ prev_ids)
+ new_id_list = add_list_uniquely(
+ list(prev_ids), parentIds)
+ new_ids = tuple(new_id_list)
+ self.work_listing[album][index] = new_ids
+ self.parts[new_ids] = self.parts[prev_ids]
+ #del self.parts[prev_ids] # Removed from here to deal with multi-parent parts. De-dup now takes place in process_albums.
+ self.parts[new_ids]['name'] = add_list_uniquely(
+ prev_name, parents)
+ parentIds = new_id_list
+ write_log(
+ release_id,
+ 'debug',
+ "In work_process. Changed wid in self.part: prev_ids = %s, new_ids = %s, prev_name = %s, new name = %s",
+ prev_ids,
+ new_ids,
+ prev_name,
+ self.parts[new_ids]['name'])
+
+
+ else:
+ self.works_cache[wid] = parentIds
+ self.parts[wid]['parent'] = parentIds
+ self.parts[tuple(parentIds)
+ ]['name'] = parents
+ self.work_listing[album].append(
+ tuple(parentIds))
+ # de-duplicate the parent names
+ # self.parts[tuple(parentIds)]['name'] = list(
+ # collections.OrderedDict.fromkeys(self.parts[tuple(parentIds)]['name']))
+ # list(set()) won't work as need to retain order
+ self.parts[tuple(parentIds)]['is_collection'] = is_collection
+ write_log(
+ release_id,
+ 'debug',
+ "In work_process. self.parts[%s]['is_collection']: %s",
+ tuple(parentIds),
+ self.parts[tuple(parentIds)]['is_collection'])
+ # de-duplicate the parent ids also, otherwise they will be treated as a separate parent
+ # in the trackback structure
+ self.parts[wid]['parent'] = list(
+ collections.OrderedDict.fromkeys(
+ self.parts[wid]['parent']))
+ self.works_cache[wid] = list(
+ collections.OrderedDict.fromkeys(
+ self.works_cache[wid]))
+ write_log(
+ release_id,
+ 'info',
+ 'Added parent ids to work_listing: %s, [Requests = %s]',
+ parentIds,
+ album._requests)
+ write_log(
+ release_id,
+ 'info',
+ 'work_listing after adding parents: %s',
+ self.work_listing[album])
+ # the higher-level work might already be in
+ # cache from another album
+ if tuple(
+ parentIds) in self.works_cache and self.USE_CACHE:
+ not_in_cache = self.check_cache(
+ track.metadata, album, track, tuple(parentIds), [])
+ for workId_tuple in not_in_cache:
+ new_queue.append(
+ (release_id, album, track, workId_tuple))
+
+ else:
+ if not self.USE_CACHE:
+ if tuple(
+ parentIds) in self.works_cache:
+ del self.works_cache[tuple(
+ parentIds)]
+ for parentId in parentIds:
+ new_queue.append(
+ (release_id, album, track, (parentId,)))
+
+ else:
+ # so we remember we looked it up and found none
+ self.parts[wid]['no_parent'] = True
+ self.top_works[(track, album)]['workId'] = wid
+ if wid not in self.top[album]:
+ self.top[album].append(wid)
+ write_log(
+ release_id, 'info', "TOP[album]: %s", self.top[album])
+ else:
+ # so we remember we looked it up and found none
+ self.parts[wid]['no_parent'] = True
+ self.top_works[(track, album)]['workId'] = wid
+ self.top[album].append(wid)
+
+ write_log(
+ release_id,
+ 'debug',
+ "End of tuple processing for workid %s in album %s, track %s,"
+ " requests remaining = %s, new queue is %r",
+ workId,
+ album,
+ track,
+ album._requests,
+ new_queue)
+ self.album_remove_request(release_id, album)
+ for queued_item in new_queue:
+ write_log(
+ release_id,
+ 'info',
+ 'Have a new queue: queued_item = %r',
+ queued_item)
+ write_log(
+ release_id,
+ 'debug',
+ 'Penultimate end of work_process for %s (subject to parent lookups in "new_queue")',
+ workId)
+ for queued_item in new_queue:
+ self.work_not_in_cache(
+ queued_item[0],
+ queued_item[1],
+ queued_item[2],
+ queued_item[3])
+ write_log(release_id, 'debug',
+ 'Ultimate end of work_process for %s', workId)
+
+ if album._requests == 0:
+ self.process_album(release_id, album)
+ album._finalize_loading(None)
+ release_status[release_id]['works-done'] = datetime.now()
+ close_log(release_id, 'works')
+
+ def work_process_metadata(self, release_id, workId, wid, track, response):
+ """
+ Process XML node
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ NB release_id may be from a different album than the original, if works lookups are identical
+ :param workId:
+ :param wid:
+ :param response:
+ :return:
+ """
+ write_log(release_id, 'debug', "In work_process_metadata")
+ all_tags = parse_data(release_id, response, [], 'tags', 'name')
+ self.parts[wid]['folks_genres'] = all_tags
+ self.parts[wid]['worktype_genres'] = parse_data(
+ release_id, response, [], 'type')
+ key = parse_data(
+ release_id,
+ response,
+ [],
+ 'attributes',
+ 'type:Key',
+ 'value')
+ self.parts[wid]['key'] = key
+ composed_begin_dates = year(
+ parse_data(
+ release_id,
+ response,
+ [],
+ 'relations',
+ 'target-type:artist',
+ 'type:composer',
+ 'begin'))
+ composed_end_dates = year(
+ parse_data(
+ release_id,
+ response,
+ [],
+ 'relations',
+ 'target-type:artist',
+ 'type:composer',
+ 'end'))
+ if composed_begin_dates == composed_end_dates:
+ composed_dates = composed_begin_dates
+ else:
+ composed_dates = list(
+ zip(composed_begin_dates, composed_end_dates))
+ composed_dates = [y + DATE_SEP + z if y != z else y for y, z in composed_dates]
+ self.parts[wid]['composed_dates'] = composed_dates
+ published_begin_dates = year(
+ parse_data(
+ release_id,
+ response,
+ [],
+ 'relations',
+ 'target-type:label',
+ 'type:publishing',
+ 'begin'))
+ published_end_dates = year(
+ parse_data(
+ release_id,
+ response,
+ [],
+ 'relations',
+ 'target-type:label',
+ 'type:publishing',
+ 'end'))
+ if published_begin_dates == published_end_dates:
+ published_dates = published_begin_dates
+ else:
+ published_dates = list(
+ zip(published_begin_dates, published_end_dates))
+ published_dates = [x + DATE_SEP + y for x, y in published_dates]
+ self.parts[wid]['published_dates'] = published_dates
+
+ premiered_begin_dates = year(
+ parse_data(
+ release_id,
+ response,
+ [],
+ 'relations',
+ 'target-type:place',
+ 'type:premiere',
+ 'begin'))
+ premiered_end_dates = year(
+ parse_data(
+ release_id,
+ response,
+ [],
+ 'relations',
+ 'target-type:place',
+ 'type:premiere',
+ 'end'))
+ if premiered_begin_dates == premiered_end_dates:
+ premiered_dates = premiered_begin_dates
+ else:
+ premiered_dates = list(
+ zip(premiered_begin_dates, premiered_end_dates))
+ premiered_dates = [x + DATE_SEP + y for x, y in premiered_dates]
+ self.parts[wid]['premiered_dates'] = premiered_dates
+
+ if 'artist_locale' in config.setting:
+ locale = config.setting["artist_locale"]
+ # NB this is the Picard code in /util
+ lang = locale.split("_")[0]
+ alias = parse_data(release_id, response, [], 'aliases',
+ 'locale:' + lang, 'primary:True', 'name')
+ user_tags = parse_data(
+ release_id, response, [], 'user-tags', 'name')
+ if config.setting['cwp_aliases_tags_user']:
+ tags = user_tags
+ else:
+ tags = all_tags
+ if alias:
+ self.parts[wid]['alias'] = self.parts[wid]['name'][:]
+ self.parts[wid]['tags'] = tags
+ for ind, w in enumerate(wid):
+ if w == workId:
+ # alias should be a one item list but...
+ self.parts[wid]['alias'][ind] = '; '.join(
+ alias)
+ relation_list = parse_data(release_id, response, [], 'relations')
+ return self.work_process_relations(
+ release_id, track, workId, wid, relation_list)
+
+ def work_process_relations(
+ self,
+ release_id,
+ track,
+ workId,
+ wid,
+ relations):
+ """
+ Find the parents etc.
+ NB track is just the last album/track for this work - used as being
+ representative for options identification. If this is inconsistent (e.g. different collections
+ option for albums with the same works) then the latest added track will over-ride others' settings).
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param track:
+ :param workId:
+ :param wid:
+ :param relations:
+ :return:
+ """
+ write_log(
+ release_id,
+ 'debug',
+ "In work_process_relations. Relations--> %s",
+ relations)
+ if track:
+ options = self.options[track]
+ else:
+ options = config.setting
+ new_workIds = []
+ new_works = []
+ attributes_list = []
+ relation_attributes = parse_data(
+ release_id,
+ relations,
+ [],
+ 'target-type:work',
+ 'type:parts',
+ 'direction:backward',
+ 'attributes')
+ new_work_list = []
+ write_log(
+ release_id,
+ 'debug',
+ "relation_attributes--> %s",
+ relation_attributes)
+ for relation_attribute in relation_attributes:
+ if (
+ 'part of collection' not in relation_attribute) or options['cwp_collections']:
+ new_work_list += parse_data(release_id,
+ relations,
+ [],
+ 'target-type:work',
+ 'type:parts',
+ 'direction:backward',
+ 'work')
+ attributes_dict = {'collection' : ('part of collection' in relation_attribute),
+ 'movements' : ('movement' in relation_attribute),
+ 'acts' : ('act' in relation_attribute),
+ 'numbers' : ('number' in relation_attribute)}
+ attributes_list += [attributes_dict]
+ if (
+ 'part of collection' in relation_attribute) and not options['cwp_collections']:
+ write_log(
+ release_id,
+ 'info',
+ 'Not getting parent work because relationship is "part of collection" and option not selected')
+ if new_work_list:
+ write_log(
+ release_id,
+ 'info',
+ 'new_work_list: %s',
+ new_work_list)
+ new_workIds = parse_data(release_id, new_work_list, [], 'id')
+ new_works = parse_data(release_id, new_work_list, [], 'title')
+ else:
+ arrangement_of = parse_data(
+ release_id,
+ relations,
+ [],
+ 'target-type:work',
+ 'type:arrangement',
+ 'direction:backward',
+ 'work')
+ if arrangement_of and options['cwp_arrangements']:
+ new_workIds = parse_data(release_id, arrangement_of, [], 'id')
+ new_works = parse_data(release_id, arrangement_of, [], 'title')
+ self.parts[wid]['arrangement'] = True
+ else:
+ medley_of = parse_data(
+ release_id,
+ relations,
+ [],
+ 'target-type:work',
+ 'type:medley',
+ 'work')
+ direction = parse_data(
+ release_id,
+ relations,
+ [],
+ 'target-type:work',
+ 'type:medley',
+ 'direction')
+ if 'backward' not in direction:
+ write_log(
+ release_id, 'info', 'Medley_of: %s', medley_of)
+ if medley_of and options['cwp_medley']:
+ medley_list = []
+ medley_id_list = []
+ for medley_item in medley_of:
+ medley_list = medley_list + \
+ parse_data(release_id, medley_item, [], 'title')
+ medley_id_list = medley_id_list + \
+ parse_data(release_id, medley_item, [], 'id')
+ # (parse_data is a list...)
+ new_workIds = medley_id_list
+ new_works = medley_list
+ write_log(
+ release_id, 'info', 'Medley_list: %s', medley_list)
+ self.parts[wid]['medley_list'] = medley_list
+
+ write_log(
+ release_id,
+ 'info',
+ 'New works: ids: %s, names: %s, attributes: %s',
+ new_workIds,
+ new_works,
+ attributes_list)
+
+ artists = get_artists(
+ options,
+ release_id,
+ {},
+ relations,
+ 'work')['artists']
+ # artist_types = ['arranger', 'instrument arranger', 'orchestrator', 'composer', 'writer', 'lyricist',
+ # 'librettist', 'revised by', 'translator', 'reconstructed by', 'vocal arranger']
+
+ write_log(release_id, 'info', "ARTISTS %s", artists)
+
+ workItems = (new_workIds, new_works, attributes_list)
+ itemsFound = [workItems, artists]
+ return itemsFound
+
+ @staticmethod
+ def album_add_request(release_id, album):
+ """
+ To keep track as to whether all lookups have been processed
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param album:
+ :return:
+ """
+ album._requests += 1
+ write_log(
+ release_id,
+ 'debug',
+ "Added album request - requests: %s",
+ album._requests)
+
+ @staticmethod
+ def album_remove_request(release_id, album):
+ """
+ To keep track as to whether all lookups have been processed
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param album:
+ :return:
+ """
+ album._requests -= 1
+ write_log(
+ release_id,
+ 'debug',
+ "Removed album request - requests: %s",
+ album._requests)
+
+ ##################################################
+ # SECTION 3 - Organise tracks and works in album #
+ ##################################################
+
+ def process_album(self, release_id, album):
+ """
+ Top routine to run end-of-album processes
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param album:
+ :return:
+ """
+ write_log(release_id, 'debug', "PROCESS ALBUM %s", album)
+ release_status[release_id]['done-lookups'] = datetime.now()
+ # De-duplicate names in self.parts, maintaining order (in case part names have been arrived at via multiple paths)
+ for part_item in self.parts:
+ if 'name' in self.parts[part_item]:
+ self.parts[part_item]['name'] = list(collections.OrderedDict.fromkeys(str_to_list(self.parts[part_item]['name'])))
+ # populate the inverse hierarchy
+ write_log(release_id, 'info', "Cache: %s", self.works_cache)
+ write_log(release_id, 'info', "Work listing %s", self.work_listing)
+ alias_tag_list = config.setting['cwp_aliases_tag_text'].split(',')
+ for i, tag_item in enumerate(alias_tag_list):
+ alias_tag_list[i] = tag_item.strip()
+ for workId in self.work_listing[album]:
+ if workId in self.parts:
+ write_log(
+ release_id,
+ 'info',
+ 'Processing workid: %s',
+ workId)
+ write_log(
+ release_id,
+ 'info',
+ 'self.work_listing[album]: %s',
+ self.work_listing[album])
+ if len(workId) > 1:
+ # fix the order of names using ordering keys gathered in
+ # work_process
+ if 'order' in self.parts[workId]:
+ seq = []
+ for idx in workId:
+ if idx in self.parts[workId]['order']:
+ seq.append(self.parts[workId]['order'][idx])
+ else:
+ # for the possibility of workids not part of
+ # the same parent and not all ordered
+ seq.append(999)
+ zipped_names = zip(self.parts[workId]['name'], seq)
+ sorted_tups = sorted(zipped_names, key=lambda x: x[1])
+ self.parts[workId]['name'] = [x[0]
+ for x in sorted_tups]
+ # use aliases where appropriate
+ # name is a list - need a string to test for Latin chars
+ name_string = '; '.join(self.parts[workId]['name'])
+ if config.setting['cwp_aliases']:
+ if config.setting['cwp_aliases_all'] or (
+ config.setting['cwp_aliases_greek'] and not only_roman_chars(name_string)) or (
+ 'tags' in self.parts[workId] and any(
+ x in self.parts[workId]['tags'] for x in alias_tag_list)):
+ if 'alias' in self.parts[workId] and self.parts[workId]['alias']:
+ self.parts[workId]['name'] = self.parts[workId]['alias'][:]
+ topId = None
+ write_log(
+ release_id,
+ 'info',
+ 'Works_cache: %s',
+ self.works_cache)
+ if workId in self.works_cache:
+ parentIds = tuple(self.works_cache[workId])
+ # for parentId in parentIds:
+ write_log(
+ release_id,
+ 'debug',
+ "Create inverses: %s, %s",
+ workId,
+ parentIds)
+ if parentIds in self.partof[album]:
+ if workId not in self.partof[album][parentIds]:
+ self.partof[album][parentIds].append(workId)
+ else:
+ self.partof[album][parentIds] = [workId]
+ write_log(release_id, 'info', "Partof: %s",
+ self.partof[album][parentIds])
+ if 'no_parent' in self.parts[parentIds]:
+ # to handle case if album includes works already in
+ # cache from a different album
+ if self.parts[parentIds]['no_parent']:
+ topId = parentIds
+ else:
+ topId = workId
+ if topId:
+ if album in self.top:
+ if topId not in self.top[album]:
+ self.top[album].append(topId)
+ else:
+ self.top[album] = [topId]
+ # work out the full hierarchy and part levels
+ height = 0
+ write_log(
+ release_id,
+ 'info',
+ "TOP: %s, \nALBUM: %s, \nTOP[ALBUM]: %s",
+ self.top,
+ album,
+ self.top[album])
+ if len(self.top[album]) > 1:
+ single_work_album = 0
+ else:
+ single_work_album = 1
+ for topId in self.top[album]:
+ self.create_trackback(release_id, album, topId)
+ write_log(
+ release_id,
+ 'info',
+ "Top id = %s, Name = %s",
+ topId,
+ self.parts[topId]['name'])
+ write_log(
+ release_id,
+ 'info',
+ "Trackback before levels: %s",
+ self.trackback[album][topId])
+ work_part_levels = self.level_calc(
+ release_id, self.trackback[album][topId], height)
+ write_log(
+ release_id,
+ 'info',
+ "Trackback after levels: %s",
+ self.trackback[album][topId])
+ # determine the level which will be the principal 'work' level
+ if work_part_levels >= 3:
+ ref_level = work_part_levels - single_work_album
+ else:
+ ref_level = work_part_levels
+ # extended metadata scheme won't display more than 3 work levels
+ # ref_level = min(3, ref_level)
+ ref_height = work_part_levels - ref_level
+ top_info = {
+ 'levels': work_part_levels,
+ 'id': topId,
+ 'name': self.parts[topId]['name'],
+ 'single': single_work_album}
+ # set the metadata in sequence defined by the work structure
+ answer = self.process_trackback(
+ release_id,
+ album,
+ self.trackback[album][topId],
+ ref_height,
+ top_info)
+ ##
+ # trackback is a tree in the form {album: {id: , children:{id: , children{},
+ # id: etc},
+ # id: etc} }
+ # process_trackback uses the trackback tree to derive title and level_0 based hierarchies
+ # from the structure. It also returns a tuple (id, tracks), where tracks has the structure
+ # {'track': [(track, height), (track, height), ...tuples...]
+ # 'work': [[worknames], [worknames], ...lists...]
+ # 'tracknumber': [num, num, ...floats of form n.nnn = disc.track...]
+ # 'title': [title, title, ...strings...]}
+ # each list is the same length - i.e. the number of tracks for the top work
+ # there can be more than one workname for a track
+ # height is the number of part levels for the related track
+ ##
+ if answer:
+ tracks = sorted(zip(answer[1]['track'], answer[1]['tracknumber']), key=lambda x: x[1])
+ # need them in tracknumber sequence for the movement numbers to be correct
+ write_log(release_id, 'info', "TRACKS: %s", tracks)
+ # work_part_levels = self.trackback[album][topId]['depth']
+ movement_count = 0
+ prev_movementgroup = None
+ for track, _ in tracks:
+ movement_count += 1
+ track_meta = track[0]
+ tm = track_meta.metadata
+ if '~cwp_workid_0' in tm:
+ workIds = tuple(str_to_list(tm['~cwp_workid_0']))
+ if workIds:
+ count = 0
+ self.process_work_artists(
+ release_id, album, track_meta, workIds, tm, count)
+ title_work_levels = 0
+ if '~cwp_title_work_levels' in tm:
+ title_work_levels = int(tm['~cwp_title_work_levels'])
+ movementgroup = self.extend_metadata(
+ release_id,
+ top_info,
+ track_meta,
+ ref_height,
+ title_work_levels) # revise for new data
+ if track_meta not in self.tracks[album]:
+ self.tracks[album][track_meta] = {}
+ if movementgroup:
+ if movementgroup != prev_movementgroup:
+ movement_count = 1
+ write_log(
+ release_id,
+ 'debug',
+ "processing movements for track: %s - movement-group is %s",
+ track, movementgroup)
+ self.tracks[album][track_meta]['movement-group'] = movementgroup
+ self.tracks[album][track_meta]['movement-number'] = movement_count
+ self.parts[tuple(movementgroup)]['movement-total'] = movement_count
+ prev_movementgroup = movementgroup
+
+ write_log(
+ release_id,
+ 'debug',
+ "FINISHED TRACK PROCESSING FOR Top work id: %s",
+ topId)
+ # Need to redo the loop so that all album-wide tm is updated before
+ # publishing
+ for track, movement_info in self.tracks[album].items():
+ self.publish_metadata(release_id, album, track, movement_info)
+ # #
+ # The messages below are normally commented out as they get VERY long if there are a lot of albums loaded
+ # For extreme debugging, remove the comments and just run one or a few albums
+ # Do not forget to comment out again.
+ # #
+ # write_log(release_id, 'info', 'Self.parts: %s', self.parts)
+ # write_log(release_id, 'info', 'Self.trackback: %s', self.trackback)
+
+ # tidy up
+ self.trackback[album].clear()
+ # Finally process the orphan tracks
+ if album in self.orphan_tracks:
+ for track in self.orphan_tracks[album]:
+ tm = track.metadata
+ options = self.options[track]
+ if options['cwp_derive_works_from_title']:
+ work, movt, inter_work = self.derive_from_title(release_id, track, tm['title'])
+ tm['~cwp_extended_work'] = tm['~cwp_extended_groupheading'] = tm['~cwp_title_work'] = \
+ tm['~cwp_title_groupheading'] = tm['~cwp_work'] = tm['~cwp_groupheading']= work
+ tm['~cwp_part'] = tm['~cwp_extended_part'] = tm['~cwp_title_part_0'] = movt
+ tm['~cwp_inter_work'] = tm['~cwp_extended_inter_work'] = tm['~cwp_inter_title_work'] = inter_work
+ self.publish_metadata(release_id, album, track)
+ write_log(release_id, 'debug', "PROCESS ALBUM function complete")
+
+ def create_trackback(self, release_id, album, parentId):
+ """
+ Create an inverse listing of the work-parent relationships
+ :param release_id:
+ :param album:
+ :param parentId:
+ :return: trackback for a given parentId
+ """
+ write_log(release_id, 'debug', "Create trackback for %s", parentId)
+ if parentId in self.partof[album]: # NB parentId is a tuple
+ for child in self.partof[album][parentId]: # NB child is a tuple
+ if child in self.partof[album]:
+ child_trackback = self.create_trackback(
+ release_id, album, child)
+ self.append_trackback(
+ release_id, album, parentId, child_trackback)
+ else:
+ self.append_trackback(
+ release_id, album, parentId, self.trackback[album][child])
+ return self.trackback[album][parentId]
+ else:
+ return self.trackback[album][parentId]
+
+ def append_trackback(self, release_id, album, parentId, child):
+ """
+ Recursive process to populate trackback
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param album:
+ :param parentId:
+ :param child:
+ :return:
+ """
+ write_log(release_id, 'debug', "In append_trackback...")
+ if parentId in self.trackback[album]: # NB parentId is a tuple
+ if 'children' in self.trackback[album][parentId]:
+ if child not in self.trackback[album][parentId]['children']:
+ write_log(release_id, 'info', "TRYING TO APPEND...")
+ self.trackback[album][parentId]['children'].append(child)
+ write_log(
+ release_id,
+ 'info',
+ "...PARENT %s - ADDED %s as child",
+ self.parts[parentId]['name'],
+ child)
+ else:
+ write_log(
+ release_id,
+ 'info',
+ "Parent %s already has %s as child",
+ parentId,
+ child)
+ else:
+ self.trackback[album][parentId]['children'] = [child]
+ write_log(
+ release_id,
+ 'info',
+ "Existing PARENT %s - ADDED %s as child",
+ self.parts[parentId]['name'],
+ child)
+ else:
+ self.trackback[album][parentId]['id'] = parentId
+ self.trackback[album][parentId]['children'] = [child]
+ write_log(
+ release_id,
+ 'info',
+ "New PARENT %s - ADDED %s as child",
+ self.parts[parentId]['name'],
+ child)
+ write_log(
+ release_id,
+ 'info',
+ "APPENDED TRACKBACK: %s",
+ self.trackback[album][parentId])
+ return self.trackback[album][parentId]
+
+ def level_calc(self, release_id, trackback, height):
+ """
+ Recursive process to determine the max level for a work
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param trackback:
+ :param height: number of levels above this one
+ :return:
+ """
+ write_log(release_id, 'debug', 'In level_calc process')
+ if 'children' not in trackback:
+ write_log(release_id, 'info', "Got to bottom")
+ trackback['height'] = height
+ trackback['depth'] = 0
+ return 0
+ else:
+ trackback['height'] = height
+ height += 1
+ max_depth = 0
+ for child in trackback['children']:
+ write_log(release_id, 'info', "CHILD: %s", child)
+ depth = self.level_calc(release_id, child, height) + 1
+ write_log(release_id, 'info', "DEPTH: %s", depth)
+ max_depth = max(depth, max_depth)
+ trackback['depth'] = max_depth
+ return max_depth
+
+ ###########################################
+ # SECTION 4 - Process tracks within album #
+ ###########################################
+
+ def process_trackback(
+ self,
+ release_id,
+ album_req,
+ trackback,
+ ref_height,
+ top_info):
+ """
+ Set work structure metadata & govern other metadata-setting processes
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param album_req:
+ :param trackback:
+ :param ref_height:
+ :param top_info:
+ :return:
+ """
+ write_log(
+ release_id,
+ 'debug',
+ "IN PROCESS_TRACKBACK. Trackback = %s",
+ trackback)
+ tracks = collections.defaultdict(dict)
+ process_now = False
+ if 'meta' in trackback:
+ for track, album in trackback['meta']:
+ if album_req == album:
+ process_now = True
+ if process_now or 'children' not in trackback:
+ if 'meta' in trackback and 'id' in trackback and 'depth' in trackback and 'height' in trackback:
+ write_log(release_id, 'info', "Processing level 0")
+ depth = trackback['depth']
+ height = trackback['height']
+ workId = tuple(trackback['id'])
+ if depth != 0:
+ if 'children' in trackback:
+ child_response = self.process_trackback_children(
+ release_id, album_req, trackback, ref_height, top_info, tracks)
+ tracks = child_response[1]
+ write_log(
+ release_id,
+ 'info',
+ 'Bottom level for this trackback is higher level elsewhere - adjusting levels')
+ depth = 0
+ write_log(release_id, 'info', "WorkId: %s, Work name: %s", workId, self.parts[workId]['name'])
+ for track, album in trackback['meta']:
+ if album == album_req:
+ write_log(release_id, 'info', "Track: %s", track)
+ tm = track.metadata
+ write_log(
+ release_id, 'info', "Track metadata = %s", tm)
+ tm['~cwp_workid_' + str(depth)] = workId
+ self.write_tags(release_id, track, tm, workId)
+ self.make_annotations(release_id, track, workId)
+ # strip leading and trailing spaces from work names
+ if isinstance(self.parts[workId]['name'], str):
+ worktemp = self.parts[workId]['name'].strip()
+ else:
+ for index, it in enumerate(
+ self.parts[workId]['name']):
+ self.parts[workId]['name'][index] = it.strip()
+ worktemp = self.parts[workId]['name']
+ if isinstance(top_info['name'], str):
+ toptemp = top_info['name'].strip()
+ else:
+ for index, it in enumerate(top_info['name']):
+ top_info['name'][index] = it.strip()
+ toptemp = top_info['name']
+ tm['~cwp_work_' + str(depth)] = worktemp
+ tm['~cwp_part_levels'] = str(height)
+ tm['~cwp_work_part_levels'] = str(top_info['levels'])
+ tm['~cwp_workid_top'] = top_info['id']
+ tm['~cwp_work_top'] = toptemp
+ tm['~cwp_single_work_album'] = top_info['single']
+ write_log(
+ release_id, 'info', "Track metadata = %s", tm)
+ if 'track' in tracks:
+ tracks['track'].append((track, height))
+ else:
+ tracks['track'] = [(track, height)]
+ tracks['tracknumber'] = [int(tm['discnumber']) + (int(tm['tracknumber']) / 1000)]
+ # Hopefully no more than 999 tracks per disc!
+ write_log(release_id, 'info', "Tracks: %s", tracks)
+
+ response = (workId, tracks)
+ write_log(release_id, 'debug', "LEAVING PROCESS_TRACKBACK")
+ write_log(
+ release_id,
+ 'info',
+ "depth %s Response = %s",
+ depth,
+ response)
+ return response
+ else:
+ return None
+ else:
+ response = self.process_trackback_children(
+ release_id, album_req, trackback, ref_height, top_info, tracks)
+ return response
+
+ def process_trackback_children(
+ self,
+ release_id,
+ album_req,
+ trackback,
+ ref_height,
+ top_info,
+ tracks):
+ """
+ TODO add some better documentation!
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param album_req:
+ :param trackback:
+ :param ref_height:
+ :param top_info:
+ :param tracks:
+ :return:
+ """
+ if 'id' in trackback and 'depth' in trackback and 'height' in trackback:
+ write_log(
+ release_id,
+ 'debug',
+ 'In process_children_trackback for trackback %s',
+ trackback)
+ depth = trackback['depth']
+ height = trackback['height']
+ parentId = tuple(trackback['id'])
+ parent = self.parts[parentId]['name']
+ width = 0
+ for child in trackback['children']:
+ width += 1
+ write_log(
+ release_id,
+ 'info',
+ "child trackback = %s",
+ child)
+ answer = self.process_trackback(
+ release_id, album_req, child, ref_height, top_info)
+ if answer:
+ workId = answer[0]
+ child_tracks = answer[1]['track']
+ for track in child_tracks:
+ track_meta = track[0]
+ track_height = track[1]
+ part_level = track_height - height
+ write_log(
+ release_id,
+ 'debug',
+ "Calling set metadata %s",
+ (part_level,
+ workId,
+ parentId,
+ parent,
+ track_meta))
+ self.set_metadata(
+ release_id, part_level, workId, parentId, parent, track_meta)
+ if 'track' in tracks:
+ tracks['track'].append(
+ (track_meta, track_height))
+ else:
+ tracks['track'] = [(track_meta, track_height)]
+ tm = track_meta.metadata
+ # ~cwp_title if composer had to be removed
+ title = tm['~cwp_title'] or tm['title']
+ if 'title' in tracks:
+ tracks['title'].append(title)
+ else:
+ tracks['title'] = [title]
+ # to make sure we get it as a list
+ work = tm.getall('~cwp_work_0')
+ if 'work' in tracks:
+ tracks['work'].append(work)
+ else:
+ tracks['work'] = [work]
+ if 'tracknumber' not in tm:
+ tm['tracknumber'] = 0
+ if 'discnumber' not in tm:
+ tm['discnumber'] = 0
+ if 'tracknumber' in tracks:
+ tracks['tracknumber'].append(
+ int(tm['discnumber']) + (int(tm['tracknumber']) / 1000))
+ else:
+ tracks['tracknumber'] = [
+ int(tm['discnumber']) + (int(tm['tracknumber']) / 1000)]
+ if tracks and 'track' in tracks:
+ track = tracks['track'][0][0]
+ # NB this will only be the first track of tracks, but its
+ # options will be used for the structure
+ self.derive_from_structure(
+ release_id, top_info, tracks, height, depth, width, 'title')
+ if self.options[track]["cwp_level0_works"]:
+ # replace hierarchical works with those from work_0 (for
+ # consistency)
+ self.derive_from_structure(
+ release_id, top_info, tracks, height, depth, width, 'work')
+
+ write_log(
+ release_id,
+ 'info',
+ "Trackback result for %s = %s",
+ parentId,
+ tracks)
+ response = parentId, tracks
+ write_log(
+ release_id,
+ 'debug',
+ "LEAVING PROCESS_CHILD_TRACKBACK depth %s Response = %s",
+ depth,
+ response)
+ return response
+ else:
+ return None
+ else:
+ return None
+
+ def derive_from_structure(
+ self,
+ release_id,
+ top_info,
+ tracks,
+ height,
+ depth,
+ width,
+ name_type):
+ """
+ Derive title (or work level-0) components from MB hierarchical work structure
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param top_info:
+ {'levels': work_part_levels,'id': topId,'name': self.parts[topId]['name'],'single': single_work_album}
+ :param tracks:
+ {'track':[(track1, height1), (track2, height2), ...], 'work': [work1, work2,...],
+ 'title': [title1, title2, ...], 'tracknumber': [tracknumber1, tracknumber2, ...]}
+ where height is the number of levels in total in the branch for that track (i.e. height 1 => work_0 & work_1)
+ :param height: number of levels above the current one
+ :param depth: maximum number of levels
+ :param width: number of siblings
+ :param name_type: work or title
+ :return:
+ """
+ if 'track' in tracks:
+ track = tracks['track'][0][0]
+ # NB this will only be the first track of tracks, but its
+ # options will be used for the structure
+ single_work_track = False # default
+ write_log(
+ release_id,
+ 'debug',
+ "Deriving info for %s from structure for tracks %s",
+ name_type,
+ tracks['track'])
+ write_log(
+ release_id,
+ 'info',
+ '%ss are %r',
+ name_type,
+ tracks[name_type])
+ if 'tracknumber' in tracks:
+ sorted_tracknumbers = sorted(tracks['tracknumber'])
+ else:
+ sorted_tracknumbers = None
+ write_log(
+ release_id,
+ 'info',
+ "SORTED TRACKNUMBERS: %s",
+ sorted_tracknumbers)
+ common_len = 0
+ if name_type in tracks:
+ meta_str = "_title" if name_type == 'title' else "_X0"
+ # in case of works, could be a list of lists
+ name_list = tracks[name_type]
+ write_log(
+ release_id,
+ 'info',
+ "%s list %s",
+ name_type,
+ name_list)
+ if len(name_list) == 1: # only one track in this work so try and extract using colons
+ single_work_track = True
+ track_height = tracks['track'][0][1]
+ if track_height - height > 0: # track_height - height == part_level
+ if name_type == 'title':
+ write_log(
+ release_id,
+ 'debug',
+ "Single track work. Deriving directly from title text: %s",
+ track)
+ ti = name_list[0]
+ common_subset = self.derive_from_title(
+ release_id, track, ti)[0]
+ else:
+ common_subset = ""
+ else:
+ common_subset = name_list[0]
+ write_log(
+ release_id,
+ 'info',
+ "%s is single-track work. common_subset is set to %s",
+ tracks['track'][0][0],
+ common_subset)
+ if common_subset:
+ common_len = len(common_subset)
+ else:
+ common_len = 0
+ else: # NB if names are lists of lists, we'll assume they all start the same way
+ if isinstance(name_list[0], list):
+ compare = name_list[0][0].split()
+ else:
+ # a list of the words in the first name
+ compare = name_list[0].split()
+ for name_item in name_list:
+ if isinstance(name_item, list):
+ name = name_item[0]
+ else:
+ name = name_item
+ lcs = longest_common_sequence(compare, name.split())
+ compare = lcs['sequence']
+ if not compare:
+ common_len = 0
+ break
+ if lcs['length'] > 0:
+ common_subset = " ".join(compare)
+ write_log(
+ release_id,
+ 'info',
+ "Common subset from %ss at level %s, item name %s ..........",
+ name_type,
+ tracks['track'][0][1] -
+ height,
+ name)
+ write_log(
+ release_id, 'info', "..........is %s", common_subset)
+ common_len = len(common_subset)
+
+ write_log(
+ release_id,
+ 'info',
+ "checked for common sequence - length is %s",
+ common_len)
+ for track_index, track_item in enumerate(tracks['track']):
+ track_meta = track_item[0]
+ tm = track_meta.metadata
+ top_level = int(tm['~cwp_part_levels'])
+ part_level = track_item[1] - height
+ if common_len > 0:
+ self.create_work_levels(release_id, name_type, tracks, track, track_index,
+ track_meta, tm, meta_str, part_level, depth, width, common_len)
+
+ else: # (no common substring at this level)
+ if name_type == 'work':
+ write_log(release_id, 'info',
+ 'single track work - indicator = %s. track = %s, part_level = %s, top_level = %s',
+ single_work_track, track_item, part_level, top_level)
+ if part_level >= top_level: # so it won't be covered by top-down action
+ for level in range(
+ 0, part_level + 1): # fill in the missing work names from the canonical list
+ if '~cwp' + meta_str + '_work_' + \
+ str(level) not in tm:
+ tm['~cwp' +
+ meta_str +
+ '_work_' +
+ str(level)] = tm['~cwp_work_' +
+ str(level)]
+ if level > 0:
+ self.level0_warn(release_id, tm, level)
+ if '~cwp' + meta_str + '_part_' + \
+ str(level) not in tm and '~cwp_part_' + str(level) in tm:
+ tm['~cwp' +
+ meta_str +
+ '_part_' +
+ str(level)] = tm['~cwp_part_' +
+ str(level)]
+ if level > 0:
+ self.level0_warn(release_id, tm, level)
+
+
+ def create_work_levels(self, release_id, name_type, tracks, track, track_index,
+ track_meta, tm, meta_str, part_level, depth, width, common_len):
+ """
+ For a group of tracks with common metadata in the title/level0 work, create the work structure
+ for that metadata, using the structure in the MB database
+ :param release_id:
+ :param name_type: title or work
+ :param tracks: {'track':[(track1, height1), (track2, height2), ...], 'work': [work1, work2,...],
+ 'title': [title1, title2, ...], 'tracknumber': [tracknumber1, tracknumber2, ...]}
+ where height is the number of levels in total in the branch for that track (i.e. height 1 => work_0 & work_1)
+ :param track:
+ :param track_index: index of track in tracks
+ :param track_meta:
+ :param tm: track meta (dup?)
+ :param meta_str: string created from name_type
+ :param part_level: The level of the current item in the works hierarchy
+ :param depth: The number of levels below the current item
+ :param width: The number of children of the current item
+ :param common_len: length of the common text
+ :return:
+ """
+ allow_repeats = True
+ write_log(
+ release_id,
+ 'info',
+ "Use %s info for track: %s at level %s",
+ name_type,
+ track_meta,
+ part_level)
+ name = tracks[name_type][track_index]
+ if isinstance(name, list):
+ work = name[0][:common_len]
+ else:
+ work = name[:common_len]
+ work = work.rstrip(":,.;- ")
+ if self.options[track]["cwp_removewords_p"]:
+ removewords = self.options[track]["cwp_removewords_p"].split(
+ ',')
+ else:
+ removewords = []
+ write_log(
+ release_id,
+ 'info',
+ "Prefixes (in %s) = %s",
+ name_type,
+ removewords)
+ for prefix in removewords:
+ prefix2 = str(prefix).lower().rstrip()
+ if prefix2[0] != " ":
+ prefix2 = " " + prefix2
+ write_log(
+ release_id, 'info', "checking prefix %s", prefix2)
+ if work.lower().endswith(prefix2):
+ if len(prefix2) > 0:
+ work = work[:-len(prefix2)]
+ common_len = len(work)
+ work = work.rstrip(":,.;- ")
+ if work.lower() == prefix2.strip():
+ work = ''
+ common_len = 0
+ write_log(
+ release_id,
+ 'info',
+ "work after prefix strip %s",
+ work)
+ write_log(release_id, 'info', "Prefixes checked")
+
+ tm['~cwp' + meta_str + '_work_' +
+ str(part_level)] = work
+
+ if part_level > 0 and name_type == "work":
+ write_log(
+ release_id,
+ 'info',
+ 'checking if %s is repeated name at part_level = %s',
+ work,
+ part_level)
+ write_log(release_id, 'info', 'lower work name is %s',
+ tm['~cwp' + meta_str + '_work_' + str(part_level - 1)])
+ # fill in missing names caused by no common string at lower levels
+ # count the missing levels and push the current name
+ # down to the lowest missing level
+ missing_levels = 0
+ fill_level = part_level - 1
+ while '~cwp' + meta_str + '_work_' + \
+ str(fill_level) not in tm:
+ missing_levels += 1
+ fill_level -= 1
+ if fill_level < 0:
+ break
+ write_log(
+ release_id,
+ 'info',
+ 'there is/are %s missing level(s)',
+ missing_levels)
+ if missing_levels > 0:
+ allow_repeats = True
+ for lev in range(
+ part_level - missing_levels, part_level):
+
+ if lev > 0: # not filled_lowest and lev > 0:
+ tm['~cwp' + meta_str +
+ '_work_' + str(lev)] = work
+ tm['~cwp' +
+ meta_str +
+ '_part_' +
+ str(lev - 1)] = self.strip_parent_from_work(track,
+ release_id,
+ interpret(tm['~cwp' + meta_str + '_work_'
+ + str(lev - 1)]),
+ tm['~cwp' + meta_str + '_work_' + str(lev)],
+ lev - 1, False)[0]
+ else:
+ tm['~cwp' + meta_str + '_work_' + str(lev)] = tm['~cwp_work_' + str(lev)]
+
+ if missing_levels > 0:
+ write_log(release_id, 'info', 'lower work name is now %r', tm.getall(
+ '~cwp' + meta_str + '_work_' + str(part_level - 1)))
+ # now fix the repeated work name at this level
+ if work == tm['~cwp' + meta_str + '_work_' +
+ str(part_level - 1)] and not allow_repeats:
+ tm['~cwp' +
+ meta_str +
+ '_work_' +
+ str(part_level)] = tm['~cwp_work_' +
+ str(part_level)]
+ self.level0_warn(release_id, tm, part_level)
+ tm['~cwp' +
+ meta_str +
+ '_part_' +
+ str(part_level -
+ 1)] = self.strip_parent_from_work(track,
+ release_id,
+ tm.getall('~cwp' + meta_str + '_work_' + str(part_level - 1)),
+ tm['~cwp' + meta_str + '_work_' + str(part_level)],
+ part_level - 1, False)[0]
+ if part_level == 1:
+ if isinstance(name, list):
+ movt = [x[common_len:].strip().lstrip(":,.;- ")
+ for x in name]
+ else:
+ movt = name[common_len:].strip().lstrip(":,.;- ")
+ write_log(
+ release_id, 'info', "%s - movt = %s", name_type, movt)
+ tm['~cwp' + meta_str + '_part_0'] = movt
+ write_log(
+ release_id,
+ 'info',
+ "%s Work part_level = %s",
+ name_type,
+ part_level)
+ if name_type == 'title':
+ if '~cwp_title_work_' + str(part_level - 1) in tm and tm['~cwp_title_work_' + str(
+ part_level)] == tm['~cwp_title_work_' + str(part_level - 1)] and width == 1:
+ pass # don't count higher part-levels which are not distinct from lower ones
+ # when the parent work has only one child
+ else:
+ tm['~cwp_title_work_levels'] = depth
+ tm['~cwp_title_part_levels'] = part_level
+ write_log(
+ release_id,
+ 'info',
+ "Set new metadata for %s OK",
+ name_type)
+
+ def level0_warn(self, release_id, tm, level):
+ """
+ Issue warnings if inadequate level 0 data
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param tm:
+ :param level:
+ :return:
+ """
+ write_log(
+ release_id,
+ 'warning',
+ 'Unable to use level 0 as work name source in level %s - using hierarchy instead',
+ level)
+ if self.WARNING:
+ self.append_tag(
+ release_id,
+ tm,
+ '~cwp_warning',
+ '5. Unable to use level 0 as work name source in level ' +
+ str(level) +
+ ' - using hierarchy instead')
+
+ def set_metadata(
+ self,
+ release_id,
+ part_level,
+ workId,
+ parentId,
+ parent,
+ track):
+ """
+ Set the names of works and parts
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param part_level:
+ :param workId:
+ :param parentId:
+ :param parent:
+ :param track:
+ :return:
+ """
+ write_log(
+ release_id,
+ 'debug',
+ "SETTING METADATA FOR TRACK = %r, parent = %s, part_level = %s",
+ track,
+ parent,
+ part_level)
+ tm = track.metadata
+ if parentId:
+ self.write_tags(release_id, track, tm, parentId)
+ self.make_annotations(release_id, track, parentId)
+ if 'annotations' in self.parts[workId]:
+ work_annotations = self.parts[workId]['annotations']
+ self.parts[workId]['stripped_annotations'] = work_annotations
+ else:
+ work_annotations = []
+ if 'annotations' in self.parts[parentId]:
+ parent_annotations = self.parts[parentId]['annotations']
+ else:
+ parent_annotations = []
+ if parent_annotations:
+ work_annotations = [
+ z for z in work_annotations if z not in parent_annotations]
+ self.parts[workId]['stripped_annotations'] = work_annotations
+
+ tm['~cwp_workid_' + str(part_level)] = parentId
+ tm['~cwp_work_' + str(part_level)] = parent
+ # maybe more than one work name
+ work = self.parts[workId]['name']
+ write_log(release_id, 'info', "Set work name to: %s", work)
+ works = []
+ # in case there is only one and it isn't in a list
+ if isinstance(work, str):
+ works.append(work)
+ else:
+ works = work[:]
+ stripped_works = []
+ for work in works:
+ extend = True
+ strip = self.strip_parent_from_work(
+ track, release_id, work, parent, part_level, extend, parentId, workId)
+
+ stripped_works.append(strip[0])
+ write_log(
+ release_id,
+ 'info',
+ "Parent: %s, Stripped works = %s",
+ parent,
+ stripped_works)
+ # now == parent, after removing full_parent logic
+ full_parent = strip[1]
+ if full_parent != parent:
+ tm['~cwp_work_' +
+ str(part_level)] = full_parent.strip()
+ self.parts[parentId]['name'] = full_parent
+ if 'no_parent' in self.parts[parentId]:
+ if self.parts[parentId]['no_parent']:
+ tm['~cwp_work_top'] = full_parent.strip()
+ tm['~cwp_part_' + str(part_level - 1)] = stripped_works
+ self.parts[workId]['stripped_name'] = stripped_works
+ write_log(release_id, 'debug', "GOT TO END OF SET_METADATA")
+
+ def write_tags(self, release_id, track, tm, workId):
+ """
+ write genre-related tags from internal variables
+ :param track:
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param tm: track metadata
+ :param workId: MBID of current work
+ :return: None - just writes tags
+ """
+ options = self.options[track]
+ candidate_genres = []
+ if options['cwp_genres_use_folks'] and 'folks_genres' in self.parts[workId]:
+ candidate_genres += self.parts[workId]['folks_genres']
+ if options['cwp_genres_use_worktype'] and 'worktype_genres' in self.parts[workId]:
+ candidate_genres += self.parts[workId]['worktype_genres']
+ self.append_tag(
+ release_id,
+ tm,
+ '~cwp_candidate_genres',
+ candidate_genres)
+ self.append_tag(release_id, tm, '~cwp_keys', self.parts[workId]['key'])
+ self.append_tag(release_id, tm, '~cwp_composed_dates',
+ self.parts[workId]['composed_dates'])
+ self.append_tag(release_id, tm, '~cwp_published_dates',
+ self.parts[workId]['published_dates'])
+ self.append_tag(release_id, tm, '~cwp_premiered_dates',
+ self.parts[workId]['premiered_dates'])
+
+ def make_annotations(self, release_id, track, wid):
+ """
+ create an 'annotations' entry in the 'parts' dict, as dictated by options, from dates and keys
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param track: the current track
+ :param wid: the current work MBID
+ :return:
+ """
+ write_log(
+ release_id,
+ 'debug',
+ "Starting module %s",
+ 'make_annotations')
+ options = self.options[track]
+ if options['cwp_workdate_include']:
+ if options['cwp_workdate_source_composed'] and 'composed_dates' in self.parts[wid] and self.parts[wid]['composed_dates']:
+ workdates = self.parts[wid]['composed_dates']
+ elif options['cwp_workdate_source_published'] and 'published_dates' in self.parts[wid] and self.parts[wid]['published_dates']:
+ workdates = self.parts[wid]['published_dates']
+ elif options['cwp_workdate_source_premiered'] and 'premiered_dates' in self.parts[wid] and self.parts[wid]['premiered_dates']:
+ workdates = self.parts[wid]['premiered_dates']
+ else:
+ workdates = []
+ else:
+ workdates = []
+ keys = []
+ if options['cwp_key_include'] and 'key' in self.parts[wid] and self.parts[wid]['key']:
+ keys = self.parts[wid]['key']
+ elif options['cwp_key_contingent_include'] and 'key' in self.parts[wid] and self.parts[wid]['key']\
+ and 'name' in self.parts[wid]:
+ write_log(
+ release_id,
+ 'info',
+ 'checking for key. keys = %s, names = %s',
+ self.parts[wid]['key'],
+ self.parts[wid]['name'])
+ # add all the parent names to the string for checking -
+ work_name = list_to_str(self.parts[wid]['name'])
+ work_chk = wid
+ while work_chk in self.works_cache:
+ parent_chk = tuple(self.works_cache[work_chk])
+ if parent_chk in self.parts and self.parts[parent_chk] and 'name' in self.parts[parent_chk] and self.parts[parent_chk]['name']:
+ parent_name = list_to_str(self.parts[parent_chk]['name'])
+ p_name_orig = self.parts[parent_chk]['name']
+ p_chk = self.parts[parent_chk]
+ work_name = parent_name + ': ' + work_name
+ work_chk = parent_chk
+ # now see if the key has been mentioned in the work or its parents
+ for key in self.parts[wid]['key']:
+ # if not any([key.lower() in x.lower() for x in
+ # str_to_list(work_name)]): # TODO remove
+ if not key.lower() in work_name.lower():
+ keys.append(key)
+ annotations = keys + workdates
+ if annotations:
+ self.parts[wid]['annotations'] = annotations
+ else:
+ if 'annotations' in self.parts[wid]:
+ del self.parts[wid]['annotations']
+ write_log(
+ release_id,
+ 'info',
+ 'make annotations has set id %s on track %s with annotation %s',
+ wid,
+ track,
+ annotations)
+ write_log(
+ release_id,
+ 'debug',
+ "Ending module %s",
+ 'make_annotations')
+
+ @staticmethod
+ def derive_from_title(release_id, track, title):
+ """
+ Attempt to parse title to get components
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param track:
+ :param title:
+ :return:
+ """
+ write_log(
+ release_id,
+ 'info',
+ "DERIVING METADATA FROM TITLE for track: %s",
+ track)
+ tm = track.metadata
+ movt = title
+ work = ""
+ colons = title.count(":")
+ inter_work = None
+ if '~cwp_part_levels' in tm:
+ part_levels = int(tm['~cwp_part_levels'])
+ if int(tm['~cwp_work_part_levels']
+ ) > 0: # we have a work with movements
+ if colons > 0:
+ title_split = title.split(': ', 1)
+ title_rsplit = title.rsplit(': ', 1)
+ if part_levels >= colons:
+ work = title_rsplit[0]
+ movt = title_rsplit[1]
+ else:
+ work = title_split[0]
+ movt = title_split[1]
+ else:
+ # No works found so try and just get parts from title
+ if colons > 0:
+ title_split = title.rsplit(': ', 1)
+ work = title_split[0]
+ if colons > 1:
+ colon_ind = work.rfind(':')
+ work = work[:colon_ind]
+ inter_work = work[colon_ind+1:]
+ movt = title_split[1]
+ write_log(release_id, 'info', "Work %s, Movt %s", work, movt)
+ return work, movt, inter_work
+
+ def process_work_artists(
+ self,
+ release_id,
+ album,
+ track,
+ workIds,
+ tm,
+ count):
+ """
+ Carry out the artist processing that needs to be done in the PartLevels class
+ as it requires XML lookups of the works
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param album:
+ :param track:
+ :param workIds:
+ :param tm:
+ :param count:
+ :return:
+ """
+ if not self.options[track]['classical_extra_artists']:
+ write_log(
+ release_id,
+ 'debug',
+ 'Not processing work_artists as ExtraArtists not selected to be run')
+ return None
+ write_log(
+ release_id,
+ 'debug',
+ 'In process_work_artists for track: %s, workIds: %s',
+ track,
+ workIds)
+ write_log(
+ release_id,
+ 'debug',
+ 'In process_work_artists for track: %s, self.parts: %s',
+ track,
+ self.parts)
+ if workIds in self.parts and 'arrangers' in self.parts[workIds]:
+ write_log(
+ release_id,
+ 'info',
+ 'Arrangers = %s',
+ self.parts[workIds]['arrangers'])
+ set_work_artists(
+ self,
+ release_id,
+ album,
+ track,
+ self.parts[workIds]['arrangers'],
+ tm,
+ count)
+ if workIds in self.works_cache:
+ count += 1
+ self.process_work_artists(release_id, album, track, tuple(
+ self.works_cache[workIds]), tm, count)
+
+ #################################################
+ # SECTION 5 - Extend work metadata using titles #
+ #################################################
+
+ def extend_metadata(self, release_id, top_info, track, ref_height, depth):
+ """
+ Combine MB work and title data according to user options
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param top_info:
+ :param track:
+ :param ref_height:
+ :param depth:
+ :return:
+ """
+ write_log(release_id, 'debug', 'IN EXTEND_METADATA')
+ tm = track.metadata
+ options = self.options[track]
+ if '~cwp_part_levels' not in tm:
+ write_log(
+ release_id,
+ 'debug',
+ 'NO PART LEVELS. Metadata = %s',
+ tm)
+ return None
+ part_levels = int(tm['~cwp_part_levels'])
+ write_log(
+ release_id,
+ 'debug',
+ "Extending metadata for track: %s, ref_height: %s, depth: %s, part_levels: %s",
+ track,
+ ref_height,
+ depth,
+ part_levels)
+ write_log(release_id, 'info', "Metadata = %s", tm)
+
+ # previously: ref_height = work_part_levels - ref_level,
+ # where this ref-level is the level for the top-named work
+ # so ref_height is effectively the "single work album" indicator (1 or 0) -
+ # i.e. where all tracks are part of one work which is implicitly the album
+ # without there being a groupheading for it
+ ref_level = part_levels - ref_height
+ # work_ref_level = work_part_levels - ref_height # not currently used
+
+ # replace works and parts by those derived from the level 0 work, where
+ # required, available and appropriate, but only use work names based on
+ # level 0 text if it doesn't cause ambiguity
+
+ # before embellishing with partial / arrangement etc
+ vanilla_part = tm['~cwp_part_0']
+
+ # Fix text for arrangements, partials and medleys (Done here so that
+ # cache can be used)
+ if options['cwp_arrangements'] and options["cwp_arrangements_text"]:
+ for lev in range(
+ 0,
+ ref_level): # top level will not be an arrangement else there would be a higher level
+ # needs to be a tuple to match
+ if '~cwp_workid_' + str(lev) in tm:
+ tup_id = tuple(str_to_list(tm['~cwp_workid_' + str(lev)]))
+ if 'arrangement' in self.parts[tup_id] and self.parts[tup_id]['arrangement']:
+ update_list = ['~cwp_work_', '~cwp_part_']
+ if options["cwp_level0_works"] and '~cwp_X0_work_' + \
+ str(lev) in tm:
+ update_list += ['~cwp_X0_work_', '~cwp_X0_part_']
+ for item in update_list:
+ tm[item + str(lev)] = options["cwp_arrangements_text"] + \
+ ' ' + tm[item + str(lev)]
+
+ if options['cwp_partial'] and options["cwp_partial_text"]:
+ if '~cwp_workid_0' in tm:
+ work0_id = tuple(str_to_list(tm['~cwp_workid_0']))
+ if 'partial' in self.parts[work0_id] and self.parts[work0_id]['partial']:
+ update_list = ['~cwp_work_0', '~cwp_part_0']
+ if options["cwp_level0_works"] and '~cwp_X0_work_0' in tm:
+ update_list += ['~cwp_X0_work_0', '~cwp_X0_part_0']
+ for item in update_list:
+ meta_item = tm.getall(item)
+ if isinstance(
+ meta_item, list): # it should be a list as I think getall always returns a list
+ if meta_item == []:
+ meta_item.append(options["cwp_partial_text"])
+ else:
+ for ind, w in enumerate(meta_item):
+ meta_item[ind] = options["cwp_partial_text"] + ' ' + w
+ write_log(
+ release_id, 'info', 'now meta item is %s', meta_item)
+ tm[item] = meta_item
+ else:
+ tm[item] = options["cwp_partial_text"] + \
+ ' ' + tm[item]
+ write_log(
+ release_id, 'info', 'meta item is not a list')
+
+ # fix "type 1" medley text
+ if options['cwp_medley']:
+ for lev in range(0, ref_level + 1):
+ if '~cwp_workid_' + str(lev) in tm:
+ tup_id = tuple(str_to_list(tm['~cwp_workid_' + str(lev)]))
+ if 'medley_list' in self.parts[tup_id] and self.parts[tup_id]['medley_list']:
+ medley_list = self.parts[tup_id]['medley_list']
+ tm['~cwp_work_' + str(lev)] += " (" + options["cwp_medley_text"] + \
+ ': ' + ', '.join(medley_list) + ")"
+ if '~cwp_part_' + str(lev) in tm:
+ tm['~cwp_part_' + str(
+ lev)] = "(" + options["cwp_medley_text"] + ") " + tm['~cwp_part_' + str(lev)]
+
+ # add any annotations for dates and keys
+ if options['cwp_workdate_include'] or options['cwp_key_include'] or options['cwp_key_contingent_include']:
+ if options["cwp_titles"] and part_levels == 0:
+ # ~cwp_title_work_0 will not have been set, but need it to hold any annotations
+ tm['~cwp_title_work_0'] = tm['~cwp_title'] or tm['title']
+ for lev in range(0, part_levels + 1):
+ if '~cwp_workid_' + str(lev) in tm:
+ tup_id = tuple(str_to_list(tm['~cwp_workid_' + str(lev)]))
+ if 'annotations' in self.parts[tup_id]:
+ write_log(
+ release_id,
+ 'info',
+ 'in extend_metadata, annotations for id %s on track %s are %s',
+ tup_id,
+ track,
+ self.parts[tup_id]['annotations'])
+ tm['~cwp_work_' + str(lev)] += " (" + \
+ ', '.join(self.parts[tup_id]['annotations']) + ")"
+ if options["cwp_level0_works"] and '~cwp_X0_work_' + \
+ str(lev) in tm:
+ tm['~cwp_X0_work_' + str(lev)] += " (" + ', '.join(
+ self.parts[tup_id]['annotations']) + ")"
+ if options["cwp_titles"] and '~cwp_title_work_' + \
+ str(lev) in tm:
+ tm['~cwp_title_work_' + str(lev)] += " (" + ', '.join(
+ self.parts[tup_id]['annotations']) + ")"
+ if lev < part_levels:
+ if 'stripped_annotations' in self.parts[tup_id]:
+ if self.parts[tup_id]['stripped_annotations']:
+ tm['~cwp_part_' + str(lev)] += " (" + ', '.join(
+ self.parts[tup_id]['stripped_annotations']) + ")"
+ if options["cwp_level0_works"] and '~cwp_X0_part_' + \
+ str(lev) in tm:
+ tm['~cwp_X0_part_' + str(lev)] += " (" + ', '.join(
+ self.parts[tup_id]['stripped_annotations']) + ")"
+ if options["cwp_titles"] and '~cwp_title_part_' + \
+ str(lev) in tm:
+ tm['~cwp_title_part' + str(lev)] += " (" + ', '.join(
+ self.parts[tup_id]['stripped_annotations']) + ")"
+
+ part = []
+ work = []
+ for level in range(0, part_levels):
+ part.append(tm['~cwp_part_' + str(level)])
+ work.append(tm['~cwp_work_' + str(level)])
+ work.append(tm['~cwp_work_' + str(part_levels)])
+
+ # Use level_0-derived names if applicable
+ if options["cwp_level0_works"]:
+ for level in range(0, part_levels + 1):
+ if '~cwp_X0_work_' + str(level) in tm:
+ work[level] = tm['~cwp_X0_work_' + str(level)]
+ else:
+ if level != 0:
+ work[level] = ''
+ if part and len(part) > level:
+ if '~cwp_X0_part_' + str(level) in tm:
+ part[level] = tm['~cwp_X0_part_' + str(level)]
+ else:
+ if level != 0:
+ part[level] = ''
+
+ # set up group heading and part
+ if part_levels > 0:
+ groupheading = work[1]
+ work_main = work[ref_level]
+ inter_work = None
+ work_titles = tm['~cwp_title_work_' + str(ref_level)]
+ if ref_level > 1:
+ for r in range(1, ref_level):
+ if inter_work:
+ inter_work = ': ' + inter_work
+ inter_work = part[r] + (inter_work or '')
+ groupheading = work[ref_level] + ':: ' + (inter_work or '')
+ else:
+ groupheading = work[0]
+ work_main = groupheading
+ inter_work = None
+ work_titles = None
+
+ # determine movement grouping (highest level that is not a collection)
+ if '~cwp_workid_top' in tm:
+ movementgroup = tuple(str_to_list(tm['~cwp_workid_top']))
+ n = part_levels
+ write_log(
+ release_id,
+ 'debug',
+ "In extend. self.parts[%s]['is_collection']: %s",
+ movementgroup,
+ self.parts[movementgroup]['is_collection'])
+ while self.parts[movementgroup]['is_collection']:
+ n -= 1
+ if n < 0:
+ # shouldn't happen in theory as bottom level can't be a collection, but just in case...
+ break
+ if '~cwp_workid_' + str(n) in tm:
+ movementgroup = tuple(str_to_list(tm['~cwp_workid_' + str(n)]))
+ else:
+ break
+
+ # set part text (initially)
+ if part:
+ part_main = part[0]
+ else:
+ part_main = work[0]
+ tm['~cwp_part'] = part_main
+
+ # fix medley text for "type 2" medleys
+ type2_medley = False
+ if self.parts[tuple(str_to_list(tm['~cwp_workid_0']))
+ ]['medley'] and options['cwp_medley']:
+ if options["cwp_medley_text"]:
+ if part_levels > 0:
+ medleyheading = groupheading + ':: ' + part[0]
+ else:
+ medleyheading = groupheading
+ groupheading = medleyheading + \
+ ' (' + options["cwp_medley_text"] + ')'
+ type2_medley = True
+
+ tm['~cwp_groupheading'] = groupheading
+ tm['~cwp_work'] = work_main
+ tm['~cwp_inter_work'] = inter_work
+ tm['~cwp_title_work'] = work_titles
+ write_log(
+ release_id,
+ 'debug',
+ "Groupheading set to: %s",
+ groupheading)
+ # extend group heading from title metadata
+ if groupheading:
+ ext_groupheading = groupheading
+ title_groupheading = None
+ ext_work = work_main
+ ext_inter_work = inter_work
+ inter_title_work = ""
+
+ if '~cwp_title_work_levels' in tm:
+
+ title_depth = int(tm['~cwp_title_work_levels'])
+ write_log(
+ release_id,
+ 'info',
+ "Title_depth: %s",
+ title_depth)
+ diff_work = [""] * ref_level
+ diff_part = [""] * ref_level
+ title_tag = [""]
+ # level 0 work for title # was 'x' # to avoid errors, reset
+ # before used
+ tw_str_lower = 'title'
+ max_d = min(ref_level, title_depth) + 1
+ for d in range(1, max_d):
+ tw_str = '~cwp_title_work_' + str(d)
+ write_log(release_id, 'info', "TW_STR = %s", tw_str)
+ if tw_str in tm:
+ title_tag.append(tm[tw_str])
+ title_work = title_tag[d]
+ work_main = ''
+ for w in range(d, ref_level + 1):
+ work_main += (work[w] + ' ')
+ diff_work[d - 1] = self.diff_pair(
+ release_id, track, tm, work_main, title_work)
+ if diff_work[d - 1]:
+ diff_work[d - 1] = diff_work[d - 1].strip('.;:-,')
+ if diff_work[d - 1] == '…':
+ diff_work[d - 1] = ''
+ if d > 1 and tw_str_lower in tm:
+ title_part = self.strip_parent_from_work(
+ track, release_id, tm[tw_str_lower], tm[tw_str], 0, False)[0]
+ if title_part:
+ title_part = title_part.strip(' .;:-,')
+ tm['~cwp_title_part_' +
+ str(d - 1)] = title_part
+ part_n = part[d - 1]
+ diff_part[d - 1] = self.diff_pair(
+ release_id, track, tm, part_n, title_part) or ""
+ if diff_part[d - 1] == '…':
+ diff_part[d - 1] = ''
+ else:
+ title_tag.append('')
+ tw_str_lower = tw_str
+ # remove duplicate items at lower levels in diff_work:
+ for w in range(ref_level - 2, -1, -1):
+ for higher in range(1, ref_level - w):
+ if diff_work[w] and diff_work[w + higher]:
+ diff_work[w] = diff_work[w].replace(
+ diff_work[w + higher], '').strip(' .;:-,\u2026')
+ # if diff_work[w] == '…':
+ # diff_work[w] = ''
+ write_log(
+ release_id,
+ 'info',
+ "diff list for works: %s",
+ diff_work)
+ write_log(
+ release_id,
+ 'info',
+ "diff list for parts: %s",
+ diff_part)
+ if not diff_work or len(diff_work) == 0:
+ if part_levels > 0:
+ ext_groupheading = groupheading
+ else:
+ write_log(
+ release_id,
+ 'debug',
+ "Now calc extended groupheading...")
+ write_log(
+ release_id,
+ 'info',
+ "depth = %s, ref_level = %s, title_depth = %s",
+ depth,
+ ref_level,
+ title_depth)
+ write_log(
+ release_id,
+ 'info',
+ "diff_work = %s, diff_part = %s",
+ diff_work,
+ diff_part)
+ # remove duplications:
+ for lev in range(1, ref_level):
+ for diff_list in [diff_work, diff_part]:
+ if diff_list[lev] and diff_list[lev - 1]:
+ diff_list[lev - 1] = self.diff_pair(
+ release_id, track, tm, diff_list[lev], diff_list[lev - 1])
+ if diff_list[lev - 1] == '…':
+ diff_list[lev - 1] = ''
+ write_log(
+ release_id,
+ 'info',
+ "Removed duplication. Revised diff_work = %s, diff_part = %s",
+ diff_work,
+ diff_part)
+ if part_levels > 0 and depth >= 1:
+ addn_work = []
+ addn_part = []
+ for stripped_work in diff_work:
+ if stripped_work:
+ write_log(
+ release_id, 'info', "Stripped work = %s", stripped_work)
+ addn_work.append(" {" + stripped_work + "}")
+ else:
+ addn_work.append("")
+ for stripped_part in diff_part:
+ if stripped_part and stripped_part != "":
+ write_log(release_id, 'info', "Stripped part = %s", stripped_part)
+ addn_part.append(" {" + stripped_part + "}")
+ else:
+ addn_part.append("")
+ write_log(
+ release_id,
+ 'info',
+ "addn_work = %s, addn_part = %s",
+ addn_work,
+ addn_part)
+ ext_groupheading = work[1] + addn_work[0]
+ ext_work = work[ref_level] + addn_work[ref_level - 1]
+ ext_inter_work = ""
+ inter_title_work = ""
+ title_groupheading = tm['~cwp_title_work_1']
+ if ref_level > 1:
+ for r in range(1, ref_level):
+ if ext_inter_work:
+ ext_inter_work = ': ' + ext_inter_work
+ ext_inter_work = part[r] + \
+ addn_work[r - 1] + ext_inter_work
+ ext_groupheading = work[ref_level] + \
+ addn_work[ref_level - 1] + ':: ' + ext_inter_work
+ if title_depth > 1 and ref_level > 1:
+ for r in range(1, min(title_depth, ref_level)):
+ if inter_title_work:
+ inter_title_work = ': ' + inter_title_work
+ inter_title_work = tm['~cwp_title_part_' +
+ str(r)] + inter_title_work
+ title_groupheading = tm['~cwp_title_work_' + str(
+ min(title_depth, ref_level))] + ':: ' + inter_title_work
+
+ else:
+ ext_groupheading = groupheading # title will be in part
+ ext_work = work_main
+ ext_inter_work = inter_work
+ inter_title_work = ""
+
+ write_log(release_id, 'debug', ".... ext_groupheading done")
+
+ if ext_groupheading:
+ write_log(
+ release_id,
+ 'info',
+ "EXTENDED GROUPHEADING: %s",
+ ext_groupheading)
+ tm['~cwp_extended_groupheading'] = ext_groupheading
+ tm['~cwp_extended_work'] = ext_work
+ if ext_inter_work:
+ tm['~cwp_extended_inter_work'] = ext_inter_work
+ if inter_title_work:
+ tm['~cwp_inter_title_work'] = inter_title_work
+ if title_groupheading:
+ tm['~cwp_title_groupheading'] = title_groupheading
+ write_log(
+ release_id,
+ 'info',
+ "title_groupheading = %s",
+ title_groupheading)
+ # extend part from title metadata
+ write_log(
+ release_id,
+ 'debug',
+ "NOW EXTEND PART...(part = %s)",
+ part_main)
+ if part_main:
+ if '~cwp_title_part_0' in tm:
+ movement = tm['~cwp_title_part_0']
+ else:
+ movement = tm['~cwp_title_part_0'] or tm['~cwp_title'] or tm['title']
+ if '~cwp_extended_groupheading' in tm:
+ work_compare = tm['~cwp_extended_groupheading'] + \
+ ': ' + part_main
+ elif '~cwp_work_1' in tm:
+ work_compare = work[1] + ': ' + part_main
+ else:
+ work_compare = work[0]
+ diff = self.diff_pair(
+ release_id, track, tm, work_compare, movement)
+ # compare with the fullest possible work name, not the stripped one
+ # - to maximise the duplication elimination
+ reverse_diff = self.diff_pair(
+ release_id, track, tm, movement, vanilla_part)
+ # for the reverse comparison use the part name without any work details or annotation
+ if diff and reverse_diff and self.parts[tuple(str_to_list(tm['~cwp_workid_0']))]['partial']:
+ diff = movement
+ # for partial tracks, do not eliminate the title text as it is
+ # frequently deliberately a component of the the overall work txt
+ # (unless it is identical)
+ fill_part = options['cwp_fill_part']
+ # To fill part with title text if it
+ # would otherwise have no text other than arrangement or partial
+ # annotations
+ if not diff and not vanilla_part and part_levels > 0 and fill_part:
+ # In other words the movement will have no text other than
+ # arrangement or partial annotations
+ diff = movement
+ write_log(release_id, 'info', "DIFF PART - MOVT. ti =%s", diff)
+ write_log(release_id,
+ 'info',
+ 'medley indicator for %s is %s',
+ tm['~cwp_workid_0'],
+ self.parts[tuple(str_to_list(tm['~cwp_workid_0']))]['medley'])
+
+ if type2_medley:
+ tm['~cwp_extended_part'] = "{" + movement + "}"
+ else:
+ if diff:
+ tm['~cwp_extended_part'] = part_main + \
+ " {" + diff.strip() + "}"
+ else:
+ tm['~cwp_extended_part'] = part_main
+ if part_levels == 0:
+ if tm['~cwp_extended_groupheading']:
+ del tm['~cwp_extended_groupheading']
+
+ # remove unwanted groupheadings (needed them up to now for adding
+ # extensions)
+ if '~cwp_groupheading' in tm and tm['~cwp_groupheading'] == tm['~cwp_part']:
+ del tm['~cwp_groupheading']
+ if '~cwp_title_groupheading' in tm and tm['~cwp_title_groupheading'] == tm['~cwp_title_part']:
+ del tm['~cwp_title_groupheading']
+ # clean up groupheadings (may be stray separators if level 0 or title
+ # options used)
+ if '~cwp_groupheading' in tm:
+ tm['~cwp_groupheading'] = tm['~cwp_groupheading'].strip(
+ ':').strip(
+ options['cwp_single_work_sep']).strip(
+ options['cwp_multi_work_sep'])
+ if '~cwp_extended_groupheading' in tm:
+ tm['~cwp_extended_groupheading'] = tm['~cwp_extended_groupheading'].strip(
+ ':').strip(
+ options['cwp_single_work_sep']).strip(
+ options['cwp_multi_work_sep'])
+ if '~cwp_title_groupheading' in tm:
+ tm['~cwp_title_groupheading'] = tm['~cwp_title_groupheading'].strip(
+ ':').strip(
+ options['cwp_single_work_sep']).strip(
+ options['cwp_multi_work_sep'])
+ write_log(release_id, 'debug', "....done")
+ return movementgroup
+
+ ##########################################################
+ # SECTION 6- Write metadata to tags according to options #
+ ##########################################################
+
+ def publish_metadata(self, release_id, album, track, movement_info={}):
+ """
+ Write out the metadata according to user options
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param album:
+ :param track:
+ :param movement_info: format is {'movement-group': movementgroup, 'movement-number': movementnumber}
+ :return:
+ """
+ write_log(release_id, 'debug', "IN PUBLISH METADATA for %s", track)
+ options = self.options[track]
+ tm = track.metadata
+ tm['~cwp_version'] = PLUGIN_VERSION
+
+ # set movement grouping tags (hidden vars)
+ if movement_info:
+ movementtotal = self.parts[tuple(movement_info['movement-group'])]['movement-total']
+ if movementtotal > 1:
+ tm['~cwp_movt_num'] = movement_info['movement-number']
+ tm['~cwp_movt_tot'] = movementtotal
+
+ # album composers needed by map_tags (set in set_work_artists)
+ if 'composer_lastnames' in self.album_artists[album]:
+ last_names = seq_last_names(self, album)
+ self.append_tag(
+ release_id,
+ tm,
+ '~cea_album_composer_lastnames',
+ last_names)
+
+ write_log(release_id, 'info', "Check options")
+ if options["cwp_titles"]:
+ write_log(release_id, 'info', "titles")
+ part = tm['~cwp_title_part_0'] or tm['~cwp_title_work_0']or tm['~cwp_title'] or tm['title']
+ # for multi-level work display
+ groupheading = tm['~cwp_title_groupheading'] or ""
+ # for single-level work display
+ work = tm['~cwp_title_work'] or ""
+ inter_work = tm['~cwp_inter_title_work'] or ""
+ elif options["cwp_works"]:
+ write_log(release_id, 'info', "works")
+ part = tm['~cwp_part']
+ groupheading = tm['~cwp_groupheading'] or ""
+ work = tm['~cwp_work'] or ""
+ inter_work = tm['~cwp_inter_work'] or ""
+ else:
+ # options["cwp_extended"]
+ write_log(release_id, 'info', "extended")
+ part = tm['~cwp_extended_part']
+ groupheading = tm['~cwp_extended_groupheading'] or ""
+ work = tm['~cwp_extended_work'] or ""
+ inter_work = tm['~cwp_extended_inter_work'] or ""
+ write_log(release_id, 'info', "Done options")
+ p1 = RE_ROMANS_AT_START
+ # Matches positive integers with punctuation
+ p2 = re.compile(r'^\W*\d+[.):-]')
+ movt = part
+ for _ in range(
+ 0, 5): # in case of multiple levels
+ movt = p2.sub('', p1.sub('', movt)).strip()
+ write_log(release_id, 'info', "Done movt")
+ movt_inc_tags = options["cwp_movt_tag_inc"].split(",")
+ movt_inc_tags = [x.strip(' ') for x in movt_inc_tags]
+ movt_exc_tags = options["cwp_movt_tag_exc"].split(",")
+ movt_exc_tags = [x.strip(' ') for x in movt_exc_tags]
+ movt_inc_1_tags = options["cwp_movt_tag_inc1"].split(",")
+ movt_inc_1_tags = [x.strip(' ') for x in movt_inc_1_tags]
+ movt_exc_1_tags = options["cwp_movt_tag_exc1"].split(",")
+ movt_exc_1_tags = [x.strip(' ') for x in movt_exc_1_tags]
+ movt_no_tags = options["cwp_movt_no_tag"].split(",")
+ movt_no_tags = [x.strip(' ') for x in movt_no_tags]
+ movt_no_sep = options["cwp_movt_no_sep"]
+ movt_tot_tags = options["cwp_movt_tot_tag"].split(",")
+ movt_tot_tags = [x.strip(' ') for x in movt_tot_tags]
+ gh_tags = options["cwp_work_tag_multi"].split(",")
+ gh_tags = [x.strip(' ') for x in gh_tags]
+ gh_sep = options["cwp_multi_work_sep"]
+ work_tags = options["cwp_work_tag_single"].split(",")
+ work_tags = [x.strip(' ') for x in work_tags]
+ work_sep = options["cwp_single_work_sep"]
+ top_tags = options["cwp_top_tag"].split(",")
+ top_tags = [x.strip(' ') for x in top_tags]
+
+ write_log(
+ release_id,
+ 'info',
+ "Done splits. gh_tags: %s, work_tags: %s, movt_inc_tags: %s, movt_exc_tags: %s, movt_no_tags: %s",
+ gh_tags,
+ work_tags,
+ movt_inc_tags,
+ movt_exc_tags,
+ movt_no_tags)
+
+ for tag in gh_tags + work_tags + movt_inc_tags + movt_exc_tags + movt_no_tags:
+ tm[tag] = ""
+ for tag in gh_tags:
+ if tag in movt_inc_tags + movt_exc_tags + movt_no_tags:
+ self.append_tag(release_id, tm, tag, groupheading, gh_sep)
+ else:
+ self.append_tag(release_id, tm, tag, groupheading)
+ for tag in work_tags:
+ if tag in movt_inc_1_tags + movt_exc_1_tags + movt_no_tags:
+ self.append_tag(release_id, tm, tag, work, work_sep)
+ else:
+ self.append_tag(release_id, tm, tag, work)
+ if '~cwp_part_levels' in tm and int(tm['~cwp_part_levels']) > 0:
+ self.append_tag(
+ release_id,
+ tm,
+ 'show work movement',
+ '1') # original tag for iTunes, kept for backwards compatibility
+ self.append_tag(
+ release_id,
+ tm,
+ 'showmovement',
+ '1') # new tag for iTunes & MusicBee, consistent with Picard tag docs
+ for tag in top_tags:
+ if '~cwp_work_top' in tm:
+ self.append_tag(release_id, tm, tag, tm['~cwp_work_top'])
+
+ if '~cwp_movt_num' in tm and len(tm['~cwp_movt_num']) > 0:
+ movt_num_punc = tm['~cwp_movt_num'] + movt_no_sep + ' '
+ else:
+ movt_num_punc = ''
+
+ for tag in movt_no_tags:
+ if tag not in movt_inc_tags + movt_exc_tags + movt_inc_1_tags + movt_exc_1_tags:
+ self.append_tag(release_id, tm, tag, tm['~cwp_movt_num'])
+
+ for tag in movt_tot_tags:
+ self.append_tag(release_id, tm, tag, tm['~cwp_movt_tot'])
+
+ for tag in movt_exc_tags:
+ if tag in movt_no_tags:
+ movt = movt_num_punc + movt
+ self.append_tag(release_id, tm, tag, movt)
+
+ for tag in movt_inc_tags:
+ if tag in movt_no_tags:
+ part = movt_num_punc + part
+ self.append_tag(release_id, tm, tag, part)
+
+
+ for tag in movt_inc_1_tags + movt_exc_1_tags:
+ if tag in movt_inc_1_tags:
+ pt = part
+ else:
+ pt = movt
+ if tag in movt_no_tags:
+ pt = movt_num_punc + pt
+ if inter_work and inter_work != "":
+ if tag in movt_exc_tags + movt_inc_tags and tag != "":
+ write_log(
+ release_id,
+ 'warning',
+ "Tag %s will have multiple contents",
+ tag)
+ if self.WARNING:
+ self.append_tag(release_id, tm, '~cwp_warning', '6. Tag ' +
+ tag +
+ ' has multiple contents')
+ self.append_tag(
+ release_id,
+ tm,
+ tag,
+ inter_work +
+ work_sep +
+ " " +
+ pt)
+ else:
+ self.append_tag(release_id, tm, tag, pt)
+
+ for tag in movt_exc_tags + movt_inc_tags + movt_exc_1_tags + movt_inc_1_tags:
+ if tag in movt_no_tags:
+ # i.e treat as one item, not multiple
+ tm[tag] = "".join(re.split('|'.join(self.SEPARATORS), tm[tag]))
+
+ # write "SongKong" tags
+ if options['cwp_write_sk']:
+ write_log(release_id, 'debug', "Writing SongKong work tags")
+ if '~cwp_part_levels' in tm:
+ part_levels = int(tm['~cwp_part_levels'])
+ for n in range(0, part_levels + 1):
+ if '~cwp_work_' + \
+ str(n) in tm and '~cwp_workid_' + str(n) in tm:
+ source = tm['~cwp_work_' + str(n)]
+ source_id = list(
+ tuple(str_to_list(tm['~cwp_workid_' + str(n)])))
+ if n == 0:
+ self.append_tag(
+ release_id, tm, 'musicbrainz_work_composition', source)
+ for source_id_item in source_id:
+ self.append_tag(
+ release_id, tm, 'musicbrainz_work_composition_id', source_id_item)
+ if n == part_levels:
+ self.append_tag(
+ release_id, tm, 'musicbrainz_work', source)
+ if 'musicbrainz_workid' in tm:
+ del tm['musicbrainz_workid']
+ # Delete the Picard version of this tag before
+ # replacing it with the SongKong version
+ for source_id_item in source_id:
+ self.append_tag(
+ release_id, tm, 'musicbrainz_workid', source_id_item)
+ if n != 0 and n != part_levels:
+ self.append_tag(
+ release_id, tm, 'musicbrainz_work_part_level' + str(n), source)
+ for source_id_item in source_id:
+ self.append_tag(
+ release_id,
+ tm,
+ 'musicbrainz_work_part_level' +
+ str(n) +
+ '_id',
+ source_id_item)
+
+ # carry out tag mapping
+ tm['~cea_works_complete'] = "Y"
+ map_tags(options, release_id, album, tm)
+
+ write_log(release_id, 'debug', "Published metadata for %s", track)
+ if options['cwp_options_tag'] != "":
+ self.cwp_options = collections.defaultdict(
+ lambda: collections.defaultdict(dict))
+
+ for opt in plugin_options('workparts') + plugin_options('genres'):
+ if 'name' in opt:
+ if 'value' in opt:
+ if options[opt['option']]:
+ self.cwp_options['Classical Extras']['Works options'][opt['name']] = opt['value']
+ else:
+ self.cwp_options['Classical Extras']['Works options'][opt['name']
+ ] = options[opt['option']]
+
+ write_log(release_id, 'info', "Options %s", self.cwp_options)
+ if options['ce_version_tag'] and options['ce_version_tag'] != "":
+ self.append_tag(release_id, tm, options['ce_version_tag'], str(
+ 'Version ' + tm['~cwp_version'] + ' of Classical Extras'))
+ if options['cwp_options_tag'] and options['cwp_options_tag'] != "":
+ self.append_tag(release_id, tm, options['cwp_options_tag'] +
+ ':workparts_options', json.loads(
+ json.dumps(
+ self.cwp_options)))
+ if self.ERROR and "~cwp_error" in tm:
+ for error in str_to_list(tm['~cwp_error']):
+ code = error[0]
+ self.append_tag(release_id, tm, '001_errors:' + code, error)
+ if self.WARNING and "~cwp_warning" in tm:
+ for warning in str_to_list(tm['~cwp_warning']):
+ wcode = warning[0]
+ self.append_tag(release_id, tm, '002_warnings:' + wcode, warning)
+
+
+ def append_tag(self, release_id, tm, tag, source, sep=None):
+ """
+ pass to main append routine
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param tm:
+ :param tag:
+ :param source:
+ :param sep: separators may be used to split string into list on appending
+ :return:
+ """
+ write_log(
+ release_id,
+ 'info',
+ "In append_tag (Work parts). tag = %s, source = %s, sep =%s",
+ tag,
+ source,
+ sep)
+ append_tag(release_id, tm, tag, source, self.SEPARATORS)
+ write_log(
+ release_id,
+ 'info',
+ "Appended. Resulting contents of tag: %s are: %s",
+ tag,
+ tm[tag])
+
+ ################################################
+ # SECTION 7 - Common string handling functions #
+ ################################################
+
+ def strip_parent_from_work(
+ self,
+ track,
+ release_id,
+ work,
+ parent,
+ part_level,
+ extend,
+ parentId=None,
+ workId=None):
+ """
+ Remove common text
+ :param track:
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param work: could be a list of works, all of which require stripping
+ :param parent:
+ :param part_level:
+ :param extend:
+ :param parentId:
+ :return:
+ """
+ # extend=True is used [ NO LONGER to find "full_parent" names] + (with parentId)
+ # to trigger recursion if unable to strip parent name from work and also to look for common subsequences
+ # extend=False is used when this routine is called for other purposes
+ # than strict work: parent relationships
+ options = self.options[track]
+ write_log(
+ release_id,
+ 'debug',
+ "STRIPPING HIGHER LEVEL WORK TEXT FROM PART NAMES")
+ write_log(
+ release_id,
+ 'info',
+ 'PARAMS: WORK = %r, PARENT = %s, PART_LEVEL = %s, EXTEND= %s',
+ work,
+ parent,
+ part_level,
+ extend)
+ if isinstance(work, list):
+ result = []
+ for w, work_item in enumerate(work):
+ if workId and isinstance(workId, list):
+ sub_workId = workId[w]
+ else:
+ sub_workId = workId
+ result.append(
+ self.strip_parent_from_work(
+ track,
+ release_id,
+ work_item,
+ parent,
+ part_level,
+ extend,
+ parentId,
+ sub_workId)[0])
+ return result, parent
+ if not isinstance(parent, str):
+ # in case it is a list - make sure it is a string
+ parent = '; '.join(parent)
+ if not isinstance(work, str):
+ work = '; '.join(work)
+
+ # replace any punctuation or numbers, with a space (to remove any
+ # inconsistent punctuation and numbering) - (?u) specifies the
+ # re.UNICODE flag in sub
+ clean_parent = re.sub("(?u)[\W]", ' ', parent)
+ # now allow the spaces to be filled with up to 2 non-letters
+ pattern_parent = clean_parent.replace(" ", "\W{0,2}")
+ pattern_parent = "(^|.*?\s)(\W*" + pattern_parent + "\W?)(.*)"
+ # (removed previous alternative pattern for extend=true, owing to catastrophic backtracking)
+ write_log(
+ release_id,
+ 'info',
+ "Pattern parent: %s, Work: %s",
+ pattern_parent,
+ work)
+ p = re.compile(pattern_parent, re.IGNORECASE | re.UNICODE)
+ m = p.search(work)
+ if m:
+ write_log(release_id, 'info', "Matched...")
+ if m.group(1):
+ stripped_work = m.group(1) + u"\u2026" + m.group(3)
+ else:
+ stripped_work = m.group(3)
+ # may not have a full work name in the parent (missing op. no.
+ # etc.)
+ stripped_work = stripped_work.lstrip(":;,.- ")
+ else:
+ write_log(release_id, 'info', "No match...")
+ stripped_work = work
+
+ if extend and options['cwp_common_chars'] > 0:
+ # try stripping out a common substring (multiple times until
+ # nothing more stripped)
+ prev_stripped_work = ''
+ counter = 1
+ while prev_stripped_work != stripped_work:
+ if counter > 20:
+ break # in case something went awry
+ prev_stripped_work = stripped_work
+ parent_tuples = self.listify(release_id, track, parent)
+ parent_words = parent_tuples['s_tuple']
+ clean_parent_words = list(parent_tuples['s_test_tuple'])
+ for w, word in enumerate(clean_parent_words):
+ clean_parent_words[w] = self.boil(release_id, word)
+ work_tuples = self.listify(
+ release_id, track, stripped_work)
+ work_words = work_tuples['s_tuple']
+ clean_work_words = list(work_tuples['s_test_tuple'])
+ for w, word in enumerate(clean_work_words):
+ clean_work_words[w] = self.boil(release_id, word)
+ common_dets = longest_common_substring(
+ clean_work_words, clean_parent_words)
+ # this is actually a list, not a string, since list
+ # arguments were supplied
+ common_seq = common_dets['string']
+ seq_length = common_dets['length']
+ seq_start = common_dets['start']
+ # the original items (before 'cleaning')
+ full_common_seq = [
+ x.group() for x in work_words[seq_start:seq_start + seq_length]]
+ # number of words in common_seq
+ full_seq_length = sum([len(x.split())
+ for x in full_common_seq])
+ write_log(
+ release_id,
+ 'info',
+ 'Checking common sequence between parent and work, iteration %s ... parent_words = %s',
+ counter,
+ parent_words)
+ write_log(
+ release_id,
+ 'info',
+ '... longest common sequence = %s',
+ common_seq)
+ if full_seq_length > 0:
+ potential_stripped_work = stripped_work
+ if seq_start > 0:
+ ellipsis = ' ' + u"\u2026" + ' '
+ else:
+ ellipsis = ''
+ if counter > 1:
+ potential_stripped_work = stripped_work.rstrip(
+ ' :,-\u2026')
+ potential_stripped_work = potential_stripped_work.replace(
+ '(\u2026)', '').rstrip()
+ potential_stripped_work = potential_stripped_work[:work_words[seq_start].start(
+ )] + ellipsis + potential_stripped_work[work_words[seq_start + seq_length - 1].end():]
+ potential_stripped_work = potential_stripped_work.lstrip(
+ ' :,-')
+ potential_stripped_work = re.sub(
+ r'(\W*…\W*)(\W*…\W*)', ' … ', potential_stripped_work)
+ potential_stripped_work = strip_excess_punctuation(
+ potential_stripped_work)
+
+ if full_seq_length >= options['cwp_common_chars'] \
+ or potential_stripped_work == '' and options['cwp_allow_empty_parts']:
+ # Make sure it is more than the required min (it will be > 0 anyway)
+ # unless a full strip will result anyway (and blank
+ # part names are allowed)
+ stripped_work = potential_stripped_work
+ if not stripped_work or stripped_work == '':
+ if workId and \
+ ('arrangement' in self.parts[workId] and self.parts[workId]['arrangement']
+ and options['cwp_arrangements'] and options['cwp_arrangements_text']) \
+ or ('partial' in self.parts[workId] and self.parts[workId]['partial']
+ and options['cwp_partial'] and options['cwp_partial_text']) \
+ and options['cwp_allow_empty_parts']:
+ pass
+ else:
+ stripped_work = prev_stripped_work # do not allow empty parts
+ counter += 1
+ stripped_work = strip_excess_punctuation(stripped_work)
+ write_log(
+ release_id,
+ 'info',
+ 'stripped_work = %s',
+ stripped_work)
+ if extend and parentId and parentId in self.works_cache:
+ write_log(
+ release_id,
+ 'info',
+ "Looking for match at next level up")
+ grandparentIds = tuple(self.works_cache[parentId])
+ grandparent = self.parts[grandparentIds]['name']
+ stripped_work = self.strip_parent_from_work(
+ track,
+ release_id,
+ stripped_work,
+ grandparent,
+ part_level,
+ True,
+ grandparentIds,
+ workId)[0]
+
+ write_log(
+ release_id,
+ 'info',
+ "Finished strip_parent_from_work, Work: %s",
+ work)
+ write_log(release_id, 'info', "Stripped work: %s", stripped_work)
+ # Changed full_parent to parent after removal of 'extend' logic above
+ stripped_work = strip_excess_punctuation(stripped_work)
+ write_log(release_id, 'info', "Stripped work after punctuation removal: %s", stripped_work)
+ return stripped_work, parent
+
+ def diff_pair(
+ self,
+ release_id,
+ track,
+ tm,
+ mb_item,
+ title_item,
+ remove_numbers=True):
+ """
+ Removes common text (or synonyms) from title item
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param track:
+ :param tm:
+ :param mb_item:
+ :param title_item:
+ :return: Reduced title item
+ """
+ write_log(release_id, 'debug', "Inside DIFF_PAIR")
+ mb = mb_item.strip()
+ write_log(release_id, 'info', "mb = %s", mb)
+ write_log(release_id, 'info', "title_item = %s", title_item)
+ if not mb:
+ write_log(
+ release_id,
+ 'info',
+ 'End of DIFF_PAIR. Returning %s',
+ None)
+ return None
+ ti = title_item.strip(" :;-.,")
+ if ti.count('"') == 1:
+ ti = ti.strip('"')
+ if ti.count("'") == 1:
+ ti = ti.strip("'")
+ write_log(release_id, 'info', "ti (amended) = %s", ti)
+ if not ti:
+ write_log(
+ release_id,
+ 'info',
+ 'End of DIFF_PAIR. Returning %s',
+ None)
+ return None
+
+ if self.options[track]["cwp_removewords_p"]:
+ removewords = self.options[track]["cwp_removewords_p"].split(',')
+ else:
+ removewords = []
+ write_log(release_id, 'info', "Prefixes = %s", removewords)
+ # remove numbers, roman numerals, part etc and punctuation from the
+ # start
+ write_log(release_id, 'info', "checking prefixes")
+ found_prefix = True
+ i = 0
+ while found_prefix:
+ if i > 20:
+ break # safety valve
+ found_prefix = False
+ for prefix in removewords:
+ if prefix[0] != " ":
+ prefix2 = str(prefix).lower().lstrip()
+ write_log(
+ release_id, 'info', "checking prefix %s", prefix2)
+ if mb.lower().startswith(prefix2):
+ found_prefix = True
+ mb = mb[len(prefix2):]
+ if ti.lower().startswith(prefix2):
+ found_prefix = True
+ ti = ti[len(prefix2):]
+ mb = mb.strip()
+ ti = ti.strip()
+ i += 1
+ write_log(
+ release_id,
+ 'info',
+ "pairs after prefix strip iteration %s. mb = %s, ti = %s",
+ i,
+ mb,
+ ti)
+ write_log(release_id, 'info', "Prefixes checked")
+
+ # replacements
+ replacements = self.replacements[track]
+ write_log(release_id, 'info', "Replacement: %s", replacements)
+ for tup in replacements:
+ for ind in range(0, len(tup) - 1):
+ ti = re.sub(tup[ind], tup[-1], ti, flags=re.IGNORECASE)
+ write_log(
+ release_id,
+ 'debug',
+ 'Looking for any new words in the title')
+
+ write_log(
+ release_id,
+ 'info',
+ "Check before splitting: mb = %s, ti = %s",
+ mb,
+ ti)
+
+ ti_tuples = self.listify(release_id, track, ti)
+ ti_tuple = ti_tuples['s_tuple']
+ ti_test_tuple = ti_tuples['s_test_tuple']
+
+ mb_tuples = self.listify(release_id, track, mb)
+ mb_test_tuple = mb_tuples['s_test_tuple']
+
+ write_log(
+ release_id,
+ 'info',
+ "Check after splitting: mb_test = %s, ti = %s, ti_test = %s",
+ mb_test_tuple,
+ ti_tuple,
+ ti_test_tuple)
+
+ ti_stencil = self.stencil(release_id, ti_tuple, ti)
+ ti_list = ti_stencil['match list']
+ ti_list_punc = ti_stencil['gap list']
+ ti_test_list = list(ti_test_tuple)
+ if ti_stencil['dummy']:
+ # to deal with case where stencil has added a dummy item at the
+ # start
+ ti_test_list.insert(0, '')
+ write_log(release_id, 'info', 'ti_test_list = %r', ti_test_list)
+ # zip is an iterable, not a list in Python 3, so make it re-usable
+ ti_zip_list = list(zip(ti_list, ti_list_punc))
+
+ # len(ti_list) should be = len(ti_test_list) as only difference should
+ # be synonyms which are each one 'word'
+ # However, because of the grouping of some words via regex, it is possible that inconsistencies might arise
+ # Therefore, there is a test here to check for equality and produce an
+ # error message (but continue processing)
+ if len(ti_list) != len(ti_test_list):
+ write_log(
+ release_id,
+ 'error',
+ 'Mismatch in title list after canonization/synonymization')
+ write_log(
+ release_id,
+ 'error',
+ 'Orig. title list = %r. Test list = %r',
+ ti_list,
+ ti_test_list)
+ # mb_test_tuple = self.listify(release_id, track, mb_test)
+ mb_list2 = list(mb_test_tuple)
+ for index, mb_bit2 in enumerate(mb_list2):
+ mb_list2[index] = self.boil(release_id, mb_bit2)
+ write_log(
+ release_id,
+ 'info',
+ "mb_list2[%s] = %s",
+ index,
+ mb_list2[index])
+ ti_new = []
+ ti_rich_list = []
+ for i, ti_bit_test in enumerate(ti_test_list):
+ if i <= len(ti_list) - 1:
+ ti_bit = ti_zip_list[i]
+ # NB ti_bit is a tuple where the word (1st item) is grouped
+ # with its following punctuation (2nd item)
+ else:
+ ti_bit = ('', '')
+ write_log(
+ release_id,
+ 'info',
+ "i = %s, ti_bit_test = %s, ti_bit = %s",
+ i,
+ ti_bit_test,
+ ti_bit)
+ ti_rich_list.append((ti_bit, True))
+ # Boolean to indicate whether ti_bit is a new word
+
+ if ti_bit_test == '':
+ ti_rich_list[i] = (ti_bit, False)
+ else:
+ if self.boil(release_id, ti_bit_test) in mb_list2:
+ ti_rich_list[i] = (ti_bit, False)
+
+ if remove_numbers: # Only remove numbers at the start if they are not new items
+ p0 = re.compile(r'\b\w+\b')
+ p1 = RE_ROMANS
+ p2 = re.compile(r'^\d+') # Matches positive integers
+ starts_with_numeral = True
+ while starts_with_numeral:
+ starts_with_numeral = False
+ if ti_rich_list and p0.match(ti_rich_list[0][0][0]):
+ start_word = p0.match(ti_rich_list[0][0][0]).group()
+ if p1.match(start_word) or p2.match(start_word):
+ if not ti_rich_list[0][1]:
+ starts_with_numeral = True
+ ti_rich_list.pop(0)
+ ti_test_list.pop(0)
+
+ write_log(
+ release_id,
+ 'info',
+ "ti_rich_list before removing singletons = %s. length = %s",
+ ti_rich_list,
+ len(ti_rich_list))
+
+ s = 0
+ index = 0
+ change = ()
+ for i, (t, n) in enumerate(ti_rich_list):
+ if n:
+ s += 1
+ index = i
+ change = t # NB this is a tuple
+
+ p = self.options[track]["cwp_proximity"]
+ ep = self.options[track]["cwp_end_proximity"]
+ # NB these may be modified later
+
+ if s == 1:
+ if 0 < index < len(ti_rich_list) - 1:
+ # ignore singleton new words in middle of title unless they are
+ # within "cwp_end_proximity" from the start or end
+ write_log(
+ release_id, 'info', 'item length is %s', len(
+ change[0].split()))
+ # also make sure that the item is just one word before
+ # eliminating
+ if ep < index < len(ti_rich_list) - ep - \
+ 1 and len(change[0].split()) == 1:
+ ti_rich_list[index] = (change, False)
+ s = 0
+
+ # remove prepositions
+ write_log(
+ release_id,
+ 'info',
+ "ti_rich_list before removing prepositions = %s. length = %s",
+ ti_rich_list,
+ len(ti_rich_list))
+ if self.options[track]["cwp_prepositions"]:
+ prepositions_fat = self.options[track]["cwp_prepositions"].split(
+ ',')
+ prepositions = [w.strip() for w in prepositions_fat]
+ for i, ti_bit_test in enumerate(
+ reversed(ti_test_list)): # Need to reverse it to check later prepositions first
+ if ti_bit_test.lower().strip() in prepositions:
+ # NB i is counting up while traversing the list backwards
+ j = len(ti_rich_list) - i - 1
+ if i == 0 or not ti_rich_list[j + 1][1]:
+ # Don't make it false if it is preceded by a
+ # non-preposition new word
+ if not (j > 0 and ti_rich_list[j -
+ 1][1] and ti_test_list[j -
+ 1].lower() not in prepositions):
+ ti_rich_list[j] = (ti_rich_list[j][0], False)
+
+ # create comparison for later usage
+ compare_string = ''
+ for item in ti_rich_list:
+ if item[1]:
+ compare_string += item[0][0]
+ ti_compare = self.boil(release_id, compare_string)
+ compare_length = len(ti_compare)
+
+ write_log(
+ release_id,
+ 'info',
+ "ti_rich_list before gapping (True indicates a word in title not in MB work) = %s. length = %s",
+ ti_rich_list,
+ len(ti_rich_list))
+ if s > 0:
+ d = p - ep
+ start = True # To keep track of new words at the start of the title
+ for i, (ti_bit, new) in enumerate(ti_rich_list):
+ if not new:
+ write_log(
+ release_id,
+ 'info',
+ "item(i = %s) val = %s - not new. proximity param = %s, end_proximity param = %s",
+ i,
+ ti_bit,
+ p,
+ ep)
+ if start:
+ prox_test = ep
+ else:
+ prox_test = p
+ if prox_test > 0:
+ for j in range(0, prox_test + 1):
+ write_log(release_id, 'info', "item(i) = %s, look-ahead(j) = %s", i, j)
+ if i + j < len(ti_rich_list):
+ if ti_rich_list[i + j][1]:
+ write_log(
+ release_id, 'info', "Set to true..")
+ ti_rich_list[i] = (ti_bit, True)
+ write_log(
+ release_id, 'info', "...set OK")
+ else:
+ if j <= p - d:
+ ti_rich_list[i] = (ti_bit, True)
+ else:
+ p = self.options[track]["cwp_proximity"]
+ start = False
+ if not ti_rich_list[i][1]:
+ p -= 1
+ ep -= 1
+ write_log(
+ release_id,
+ 'info',
+ "ti_rich_list after gapping (True indicates new words plus infills) = %s",
+ ti_rich_list)
+ nothing_new = True
+ for (ti_bit, new) in ti_rich_list:
+ if new:
+ nothing_new = False
+ new_prev = True
+ break
+ if nothing_new:
+ write_log(
+ release_id,
+ 'info',
+ 'End of DIFF_PAIR. Returning %s',
+ None)
+ return None
+ else:
+ new_prev = False
+ for i, (ti_bit, new) in enumerate(ti_rich_list):
+ write_log(release_id, 'info', "Create new for %s?", ti_bit)
+ if new:
+ write_log(release_id, 'info', "Yes for %s", ti_bit)
+ if not new_prev:
+ if i > 0:
+ # check to see if the last char of the prev
+ # punctuation group needs to be added first
+ if len(ti_rich_list[i - 1][0][1]) > 1:
+ # i.e. ti_bit[1][-1] of previous loop
+ ti_new.append(ti_rich_list[i - 1][0][1][-1])
+ ti_new.append(ti_bit[0])
+ if len(ti_bit[1]) > 1:
+ if i < len(ti_rich_list) - 1:
+ if ti_rich_list[i + 1][1]:
+ ti_new.append(ti_bit[1])
+ else:
+ ti_new.append(ti_bit[1][:-1])
+ else:
+ ti_new.append(ti_bit[1])
+ else:
+ ti_new.append(ti_bit[1])
+ write_log(
+ release_id,
+ 'info',
+ "appended %s. ti_new is now %s",
+ ti_bit,
+ ti_new)
+ else:
+ write_log(release_id, 'info', "Not for %s", ti_bit)
+ if new != new_prev:
+ ti_new.append(u"\u2026" + ' ')
+
+ new_prev = new
+ if ti_new:
+ write_log(release_id, 'info', "ti_new %s", ti_new)
+ ti = ''.join(ti_new)
+ write_log(release_id, 'info', "New text from title = %s", ti)
+ else:
+ write_log(release_id, 'info', "New text empty")
+ write_log(
+ release_id,
+ 'info',
+ 'End of DIFF_PAIR. Returning %s',
+ None)
+ return None
+ # see if there is any significant difference between the strings
+ if ti:
+ nopunc_ti = ti_compare # was = self.boil(release_id, ti)
+ # not necessary as already set?
+ nopunc_mb = self.boil(release_id, mb)
+ # ti_len = len(nopunc_ti) use compare_length instead (= len before
+ # removals and additions)
+ substring_proportion = float(
+ self.options[track]["cwp_substring_match"]) / 100
+ sub_len = compare_length * substring_proportion
+ if substring_proportion < 1:
+ write_log(release_id, 'info', "test sub....")
+ lcs = longest_common_substring(nopunc_mb, nopunc_ti)['string']
+ write_log(
+ release_id,
+ 'info',
+ "Longest common substring is: %s. Threshold length is %s",
+ lcs,
+ sub_len)
+ if len(lcs) >= sub_len:
+ write_log(
+ release_id,
+ 'info',
+ 'End of DIFF_PAIR. Returning %s',
+ None)
+ return None
+ write_log(release_id, 'info', "...done, ti =%s", ti)
+ # remove duplicate successive words (and remove first word of title
+ # item if it duplicates last word of mb item)
+ if ti:
+ ti_list_new = re.split(' ', ti)
+ ti_list_ref = ti_list_new
+ ti_bit_prev = None
+ for i, ti_bit in enumerate(ti_list_ref):
+ if ti_bit != "...":
+
+ if i > 1:
+ if self.boil(
+ release_id, ti_bit) == self.boil(
+ release_id, ti_bit_prev):
+ dup = ti_list_new.pop(i)
+ write_log(release_id, 'info', "...removed dup %s", dup)
+
+ ti_bit_prev = ti_bit
+
+ write_log(release_id,
+ 'info',
+ "1st word of ti = %s. Last word of mb = %s",
+ ti_list_new[0],
+ mb_list2[-1])
+ if ti_list_new and mb_list2:
+ if self.boil(release_id, ti_list_new[0]) == mb_list2[-1]:
+ write_log(release_id, 'info', "Removing 1st word from ti...")
+ first = ti_list_new.pop(0)
+ write_log(release_id, 'info', "...removed %s", first)
+ else:
+ write_log(
+ release_id,
+ 'info',
+ 'End of DIFF_PAIR. Returning %s',
+ None)
+ return None
+ if ti_list_new:
+ ti = ' '.join(ti_list_new)
+ else:
+ write_log(
+ release_id,
+ 'info',
+ 'End of DIFF_PAIR. Returning %s',
+ None)
+ return None
+ # remove excess brackets and punctuation
+ if ti:
+ ti = strip_excess_punctuation(ti)
+ write_log(release_id, 'info', "stripped punc ok. ti = %s", ti)
+ write_log(
+ release_id,
+ 'debug',
+ "DIFF_PAIR is returning ti = %s",
+ ti)
+ if ti and len(ti) > 0:
+ write_log(
+ release_id,
+ 'info',
+ 'End of DIFF_PAIR. Returning %s',
+ ti)
+ return ti
+ else:
+ write_log(
+ release_id,
+ 'info',
+ 'End of DIFF_PAIR. Returning %s',
+ None)
+ return None
+
+
+ @staticmethod
+ def canonize_opus(release_id, track, s):
+ """
+ make opus numbers etc. into one-word items
+ :param release_id:
+ :param s: A string
+ :return:
+ """
+ write_log(release_id, 'debug', 'Canonizing: %s', s)
+ # Canonize catalogue & opus numbers (e.g. turn K. 126 into K126 or K
+ # 345a into K345a or op. 144 into op144):
+ regex = re.compile(
+ r'\b((?:op|no|k|kk|kv|L|B|Hob|S|D|M)|\w+WV)\W?\s?(\d+\-?\u2013?\u2014?\d*\w*)\b',
+ re.IGNORECASE)
+ regex_match = regex.search(s)
+ s_canon = s
+ if regex_match and len(regex_match.groups()) == 2:
+ pt1 = regex_match.group(1) or ''
+ pt2 = regex_match.group(2) or ''
+ if regex_match.group(1) and regex_match.group(2):
+ pt1 = re.sub(
+ r'^\W*no\b',
+ '',
+ regex_match.group(1),
+ flags=re.IGNORECASE)
+ s_canon = pt1 + pt2
+ write_log(release_id, 'info', 'canonized item = %s', s_canon)
+ return s_canon
+
+ @staticmethod
+ def canonize_key(release_id, track, s):
+ """
+ make keys into standardized one-word items
+ :param release_id:
+ :param s: A string
+ :return:
+ """
+ write_log(release_id, 'debug', 'Canonizing: %s', s)
+ match = RE_KEYS.search(s)
+ s_canon = s
+ if match:
+ if match.group(2):
+ k2 = re.sub(
+ r'\-sharp|\u266F',
+ 'sharp',
+ match.group(2),
+ flags=re.IGNORECASE)
+ k2 = re.sub(r'\-flat|\u266D', 'flat', k2, flags=re.IGNORECASE)
+ k2 = k2.replace('-', '')
+ else:
+ k2 = ''
+ if not match.group(3) or match.group(
+ 3).strip() == '': # if the scale is not given, assume it is the major key
+ if match.group(1).isupper(
+ ) or k2 != '': # but only if it is upper case or has an accent
+ k3 = 'major'
+ else:
+ k3 = ''
+ else:
+ k3 = match.group(3).strip()
+ s_canon = match.group(1).strip() + k2.strip() + k3
+ write_log(release_id, 'info', 'canonized item = %s', s_canon)
+ return s_canon
+
+ @staticmethod
+ def canonize_synonyms(release_id, tuples, s):
+ """
+ make synonyms equal
+ :param release_id:
+ :param tuples
+ :param s: A string
+ :return:
+ """
+ write_log(release_id, 'debug', 'Canonizing: %s', s)
+ s_canon = s
+ syn_patterns = []
+ syn_subs = []
+ for syn_tup in tuples:
+ syn_pattern = r'((?:^|\W)' + \
+ r'(?:$|\W)|(?:^|\W)'.join(syn_tup) + r'(?:$|\W))'
+ syn_patterns.append(syn_pattern)
+ # to get the last synonym in the tuple - the canonical form
+ syn_sub = syn_tup[-1:][0]
+ syn_subs.append(syn_sub)
+ for syn_ind, pattern in enumerate(syn_patterns):
+ regex = re.compile(pattern, re.IGNORECASE)
+ regex_match = regex.search(s)
+ if regex_match:
+ test_reg = regex_match.group().strip()
+ s_canon = s_canon.replace(test_reg, syn_subs[syn_ind])
+
+ write_log(release_id, 'info', 'canonized item = %s', s_canon)
+ return s_canon
+
+ def find_synonyms(self, release_id, track, reg_item):
+ """
+ extend regex item to include synonyms
+ :param release_id:
+ :param track:
+ :param reg_item: A regex portion
+ :return: reg_new: A replacement for reg_item that includes all its synonyms
+ (if reg_item matches the last in a synonym tuple)
+ """
+ write_log(release_id, 'debug', 'Finding synonyms of: %s', reg_item)
+ syn_others = []
+ syn_all = []
+ for syn_tup in self.synonyms[track]:
+ # to get the last synonym in the tuple - the canonical form
+ syn_last = syn_tup[-1:][0]
+ if re.match(r'^\s*' + reg_item + r'\s*$', syn_last, re.IGNORECASE):
+ syn_others += syn_tup[:-1]
+ syn_all += syn_tup
+ if syn_others:
+ reg_item = '(?:' + ')|(?:'.join(syn_others) + \
+ ')|(?:' + reg_item + ')'
+
+ write_log(release_id, 'info', 'new regex item = %s', reg_item)
+ return reg_item, syn_all
+
+ def listify(self, release_id, track, s):
+ """
+ Turn a string into a list of 'words', where words may also be phrases which
+ are then 'canonized' - i.e. turned into equivalents for comparison purposes
+ :param release_id:
+ :param s: string
+ :return: s_tuple: a tuple of all the **match objects** (re words and defined phrases)
+ s_test_tuple: a tuple of the matched and canonized words and phrases (i.e. a tuple of strings, not objects)
+ """
+ tuples = self.synonyms[track]
+ # just list anything that is a synonym (with word boundary markers)
+ syn_pattern = '|'.join(
+ [r'(?:^|\W|\b)' + x + r'(?:$|\W)' for y in self.synonyms[track] for x in y])
+ op = self.find_synonyms(
+ release_id,
+ track,
+ r'(?:op|no|k|kk|kv|L|B|Hob|S|D|M|\w+WV)')
+ op_groups = op[0]
+ op_all = op[1]
+ notes = self.find_synonyms(release_id, track, r'[ABCDEFG]')
+ notes_groups = notes[0]
+ notes_all = notes[1]
+ sharp = self.find_synonyms(release_id, track, r'sharp')
+ sharp_groups = sharp[0]
+ sharp_all = sharp[1]
+ flat = self.find_synonyms(release_id, track, r'flat')
+ flat_groups = flat[0]
+ flat_all = flat[1]
+ major = self.find_synonyms(release_id, track, r'major')
+ major_groups = major[0]
+ major_all = major[1]
+ minor = self.find_synonyms(release_id, track, r'minor')
+ minor_groups = minor[0]
+ minor_all = minor[1]
+ opus_pattern = r"(?:\b((?:(" + op_groups + \
+ r"))\W?\s?\d+\-?\u2013?\u2014?\d*\w*)\b)"
+ note_pattern = r"(\b" + notes_groups + r")"
+ accent_pattern = r"(?:\-(" + sharp_groups + r")(?:\s+|\b)|\-(" + flat_groups + r")(?:\s+|\b)|\s(" + sharp_groups + \
+ r")(?:\s+|\b)|\s(" + flat_groups + r")(?:\s+|\b)|\u266F(?:\s+|\b)|\u266D(?:\s+|\b)|(?:[:,.]?\s+|$|\-))"
+ scale_pattern = r"(?:((" + major_groups + \
+ r")|(" + minor_groups + r"))?\b)"
+ key_pattern = note_pattern + accent_pattern + scale_pattern
+ hyphen_split_pattern = r"(?:\b|\"|\')(\w+['’]?\w*)|(?:\b\w+\b)|(\B\&\B)"
+ # treat em-dash and en-dash as hyphens
+ hyphen_embed_pattern = r"(?:\b|\"|\')(\w+['’\-\u2013\u2014]?\w*)|(?:\b\w+\b)|(\B\&\B)"
+
+ # The regex is split into two iterations as putting it all together can have unpredictable consequences
+ # - may match synonyms before op's even though that is later in the string
+
+ # First match the op's and keys
+ regex_1 = opus_pattern + r"|(" + key_pattern + r")"
+ matches_1 = re.finditer(regex_1, s, re.UNICODE | re.IGNORECASE)
+ s_list = []
+ s_test_list = []
+ s_scrubbed = s
+ all_synonyms_lists = [
+ op_all,
+ notes_all,
+ sharp_all,
+ flat_all,
+ sharp_all,
+ flat_all,
+ major_all,
+ minor_all]
+ matches_list = [2, 4, 5, 6, 7, 8, 10, 11]
+ for match in matches_1:
+ test_a = match.group()
+ match_a = []
+ match_a.append(match.group())
+ for j in range(1, 12):
+ match_a.append(match.group(j))
+ # 0. overall match
+ # 1. overall opus match
+ # 2. 2-char op match
+ # 3. overall key match
+ # 4. note match
+ # 5. hyphenated sharp match
+ # 6. hyphenated flat match
+ # 7. non-hyphenated sharp match
+ # 8. non-hyphenated flat match
+ # 9. overall scale match
+ # 10. major match
+ # 11. minor match
+ for i, all_synonyms_list in enumerate(all_synonyms_lists):
+ if all_synonyms_list and match_a[matches_list[i]]:
+ match_regex = [re.match(pattern, match_a[matches_list[i]], re.IGNORECASE).group()
+ for pattern in all_synonyms_list
+ if re.match(pattern, match_a[matches_list[i]], re.IGNORECASE)]
+ if match_regex:
+ match_a[matches_list[i]] = self.canonize_synonyms(
+ release_id, tuples, match_a[matches_list[i]])
+ test_a = re.sub(r"\b" + match_regex[0] + r"(?:\b|$|\s|\.)",
+ match_a[matches_list[i]],
+ test_a, flags=re.IGNORECASE)
+ if match_a[1]:
+ clean_opus = test_a.strip(' ,.:;/-?"')
+ test_a = re.sub(
+ re.escape(clean_opus),
+ self.canonize_opus(
+ release_id,
+ track,
+ clean_opus),
+ test_a,
+ flags=re.IGNORECASE)
+ if match_a[3]:
+ clean_key = test_a.strip(' ,.:;/-?"')
+ test_a = re.sub(
+ re.escape(clean_key),
+ self.canonize_key(
+ release_id,
+ track,
+ clean_key),
+ test_a,
+ flags=re.IGNORECASE)
+
+ s_test_list.append(test_a)
+ s_list.append(match)
+ s_scrubbed_list = list(s_scrubbed)
+ for char in range(match.start(), match.end()):
+ if len(s_scrubbed_list) >= match.end(): # belt and braces
+ s_scrubbed_list[char] = '#'
+ s_scrubbed = ''.join(s_scrubbed_list)
+
+ # Then match the synonyms and remaining words
+ if self.options[track]["cwp_split_hyphenated"]:
+ regex_2 = r"(" + syn_pattern + r")|" + hyphen_split_pattern
+ # allow ampersands and non-latin characters as word characters. Treat apostrophes as part of words.
+ # Treat opus and catalogue entries - e.g. K. 657 or OP.5 or op. 35a or CD 144 or BWV 243a - as one word
+ # also treat ranges of opus numbers (connected by dash, en dash or
+ # em dash) as one word
+ else:
+ regex_2 = r"(" + syn_pattern + r")|" + hyphen_embed_pattern
+ # as previous but also treat embedded hyphens as part of words.
+ matches_2 = re.finditer(
+ regex_2, s_scrubbed, re.UNICODE | re.IGNORECASE)
+ for match in matches_2:
+ if match.group(1) and match.group(1) == match.group():
+ s_test_list.append(
+ self.canonize_synonyms(
+ release_id,
+ tuples,
+ match.group(1))) # synonym
+ else:
+ s_test_list.append(match.group())
+ s_list.append(match)
+ if s_list:
+ s_zip = list(zip(s_list, s_test_list))
+ s_list, s_test_list = zip(
+ *sorted(s_zip, key=lambda tup: tup[0].start()))
+ s_tuple = tuple(s_list)
+ s_test_tuple = tuple(s_test_list)
+ return {'s_tuple': s_tuple, 's_test_tuple': s_test_tuple}
+
+ def get_text_tuples(self, release_id, track, text_type):
+ """
+ Return synonym or 'replacement' tuples
+ :param release_id:
+ :param track:
+ :param text_type: 'replacements' or 'synonyms'
+ Note that code in this method refers to synonyms (as that was written first), but applies equally to replacements and ui_tags
+ :return:
+ """
+ tm = track.metadata
+ strsyns = re.split(r'(?= 2:
+ for i, ts in enumerate(tup):
+ tup[i] = ts.strip("' ").strip('"')
+ if len(
+ tup[i]) > 4 and tup[i][0] == "!" and tup[i][1] == "!" and tup[i][-1] == "!" and tup[i][-2] == "!":
+ # we have a reg ex inside - this deals with legacy
+ # replacement text where enclosure in double-shouts was
+ # required
+ tup[i] = tup[i][2:-2]
+ if (i < len(tup) - 1 or text_type ==
+ 'synonyms') and not tup[i]:
+ write_log(
+ release_id,
+ 'warning',
+ '%s: entries must not be blank - error in %s',
+ text_type,
+ syn)
+ if self.WARNING:
+ self.append_tag(
+ release_id,
+ tm,
+ '~cwp_warning',
+ '7. ' + text_type + ': entries must not be blank - error in ' + syn)
+ tup[i] = "**BAD**"
+ elif [tup for t in synonyms if tup[i] in t]:
+ write_log(
+ release_id,
+ 'warning',
+ '%s: keys cannot duplicate any in existing %s - error in %s '
+ '- omitted from %s. To fix, place all %s in one tuple.',
+ text_type,
+ text_type,
+ syn,
+ text_type,
+ text_type)
+ if self.WARNING:
+ self.append_tag(release_id, tm, '~cwp_warning',
+ '7. ' + text_type + ': keys cannot duplicate any in existing ' + text_type + ' - error in ' +
+ syn + ' - omitted from ' + text_type + '. To fix, place all ' + text_type + ' in one tuple.')
+ tup[i] = "**BAD**"
+ if "**BAD**" in tup:
+ continue
+ else:
+ synonyms.append(tup)
+ else:
+ write_log(
+ release_id,
+ 'warning',
+ 'Error in %s format for %s',
+ text_type,
+ syn)
+ if self.WARNING:
+ self.append_tag(
+ release_id,
+ tm,
+ '~cwp_warning',
+ '7. Error in ' +
+ text_type +
+ ' format for ' +
+ syn)
+ write_log(release_id, 'info', "%s: %s", text_type, synonyms)
+ return synonyms
+
+ @staticmethod
+ def stencil(release_id, matches_tuple, test_string):
+ """
+ Produce lists of matching items, AND the items in between, in equal length lists
+ :param release_id:
+ :param matches_tuple: tuple of regex matches
+ :param test_string: original string used in regex
+ :return: 'match list' - list of matched strings, 'gap list' - list of strings in gaps between matches
+ """
+ match_items = []
+ gap_items = []
+ dummy = False
+ pointer = 0
+ write_log(
+ release_id,
+ 'debug',
+ 'In fn stencil. test_string = %s. matches_tuple = %s',
+ test_string,
+ matches_tuple)
+ for match_num, match in enumerate(matches_tuple):
+ start = match.start()
+ end = match.end()
+ if start > pointer:
+ if pointer == 0:
+ # add a null word item at start to keep the lists the same
+ # length
+ match_items.append('')
+ dummy = True
+ gap_items.append(test_string[pointer:start])
+ else:
+ if pointer > 0:
+ # shouldn't happen, but just in case there are two word
+ # items with no gap
+ gap_items.append('')
+ match_items.append(test_string[start:end])
+ pointer = end
+ if match_num + 1 == len(matches_tuple):
+ # pick up any punc items at end
+ gap_items.append(test_string[pointer:])
+ return {
+ 'match list': match_items,
+ 'gap list': gap_items,
+ 'dummy': dummy}
+
+ def boil(self, release_id, s):
+ """
+ Remove punctuation, spaces, capitals and accents for string comparisons
+ :param release_id: name for log file - usually =musicbrainz_albumid
+ unless called outside metadata processor
+ :param s:
+ :return:
+ """
+ write_log(release_id, 'debug', "boiling %s", s)
+ s = s.lower()
+ s = replace_roman_numerals(s)
+ s = s.replace('sch', 'sh')\
+ .replace(u'\xdf', 'ss')\
+ .replace('sz', 'ss')\
+ .replace(u'\u0153', 'oe')\
+ .replace('oe', 'o')\
+ .replace(u'\u00fc', 'ue')\
+ .replace('ue', 'u')\
+ .replace(u'\u00e6', 'ae')\
+ .replace('ae', 'a')\
+ .replace(u'\u266F', 'sharp')\
+ .replace(u'\u266D', 'flat')\
+ .replace(u'\u2013', '-')\
+ .replace(u'\u2014', '-')
+ # first term above is to remove the markers used for synonyms, to
+ # enable a true comparison
+ punc = re.compile(r'\W*', re.ASCII)
+ s = ''.join(
+ c for c in unicodedata.normalize(
+ 'NFD',
+ s) if unicodedata.category(c) != 'Mn')
+ boiled = punc.sub('', s).strip().lower().rstrip("s'")
+ write_log(release_id, 'debug', "boiled result = %s", boiled)
+ return boiled
+
+
+################
+# OPTIONS PAGE #
+################
+
+class ClassicalExtrasOptionsPage(OptionsPage):
+ NAME = "classical_extras"
+ TITLE = "Classical Extras"
+ PARENT = "plugins"
+ opts = plugin_options('artists') + plugin_options('tag') + plugin_options('tag_detail') +\
+ plugin_options('workparts') + plugin_options('genres') + plugin_options('other')
+
+ options = [
+ IntOption("persist", 'ce_tab', 0)
+ ]
+ # custom logging for non-album-related messages is written to session.log
+ for opt in opts:
+ if 'type' in opt:
+ if 'default' in opt:
+ default = opt['default']
+ else:
+ default = ""
+ if opt['type'] == 'Boolean':
+ options.append(BoolOption("setting", opt['option'], default))
+ elif opt['type'] == 'Text' or opt['type'] == 'Combo' or opt['type'] == 'PlainText':
+ options.append(TextOption("setting", opt['option'], default))
+ elif opt['type'] == 'Integer':
+ options.append(IntOption("setting", opt['option'], default))
+ else:
+ write_log(
+ "session",
+ 'error',
+ "Error in setting options for option = %s",
+ opt['option'])
+
+ def __init__(self, parent=None):
+ super(ClassicalExtrasOptionsPage, self).__init__(parent)
+ self.ui = Ui_ClassicalExtrasOptionsPage()
+ self.ui.setupUi(self)
+
+ def load(self):
+ """
+ Load the options - NB all options are set in plugin_options, so this just parses that
+ :return:
+ """
+ opts = plugin_options('artists') + plugin_options('tag') + plugin_options('tag_detail') +\
+ plugin_options('workparts') + plugin_options('genres') + plugin_options('other')
+
+ # To force a toggle so that signal given
+ toggle_list = ['use_cwp',
+ 'use_cea',
+ 'cea_override',
+ 'cwp_override',
+ 'cea_ra_use',
+ 'cea_split_lyrics',
+ 'cwp_partial',
+ 'cwp_arrangements',
+ 'cwp_medley',
+ 'cwp_use_muso_refdb',
+ 'ce_show_ui_tags',]
+
+ # open at last used tab
+ if 'ce_tab' in config.persist:
+ cfg_val = config.persist['ce_tab'] or 0
+ if 0 <= cfg_val <= 5:
+ self.ui.tabWidget.setCurrentIndex(cfg_val)
+ else:
+ self.ui.tabWidget.setCurrentIndex(0)
+
+ for opt in opts:
+ if opt['option'] == 'classical_work_parts':
+ ui_name = 'use_cwp'
+ elif opt['option'] == 'classical_extra_artists':
+ ui_name = 'use_cea'
+ else:
+ ui_name = opt['option']
+ if ui_name in toggle_list:
+ not_setting = not self.config.setting[opt['option']]
+ self.ui.__dict__[ui_name].setChecked(not_setting)
+
+ if opt['type'] == 'Boolean':
+ self.ui.__dict__[ui_name].setChecked(
+ self.config.setting[opt['option']])
+ elif opt['type'] == 'Text':
+ self.ui.__dict__[ui_name].setText(
+ self.config.setting[opt['option']])
+ elif opt['type'] == 'PlainText':
+ self.ui.__dict__[ui_name].setPlainText(
+ self.config.setting[opt['option']])
+ elif opt['type'] == 'Combo':
+ self.ui.__dict__[ui_name].setEditText(
+ self.config.setting[opt['option']])
+ elif opt['type'] == 'Integer':
+ self.ui.__dict__[ui_name].setValue(
+ self.config.setting[opt['option']])
+ else:
+ write_log(
+ 'session',
+ 'error',
+ "Error in loading options for option = %s",
+ opt['option'])
+
+ def save(self):
+ opts = plugin_options('artists') + plugin_options('tag') + plugin_options('tag_detail') +\
+ plugin_options('workparts') + plugin_options('genres') + plugin_options('other')
+
+ # save tab setting
+ config.persist['ce_tab'] = self.ui.tabWidget.currentIndex()
+
+ for opt in opts:
+ if opt['option'] == 'classical_work_parts':
+ ui_name = 'use_cwp'
+ elif opt['option'] == 'classical_extra_artists':
+ ui_name = 'use_cea'
+ else:
+ ui_name = opt['option']
+ if opt['type'] == 'Boolean':
+ self.config.setting[opt['option']] = self.ui.__dict__[
+ ui_name].isChecked()
+ elif opt['type'] == 'Text':
+ self.config.setting[opt['option']] = str(
+ self.ui.__dict__[ui_name].text())
+ elif opt['type'] == 'PlainText':
+ self.config.setting[opt['option']] = str(
+ self.ui.__dict__[ui_name].toPlainText())
+ elif opt['type'] == 'Combo':
+ self.config.setting[opt['option']] = str(
+ self.ui.__dict__[ui_name].currentText())
+ elif opt['type'] == 'Integer':
+ self.config.setting[opt['option']
+ ] = self.ui.__dict__[ui_name].value()
+ else:
+ write_log(
+ 'session',
+ 'error',
+ "Error in saving options for option = %s",
+ opt['option'])
+
+
+#################
+# MAIN ROUTINE #
+#################
+
+# custom logging for non-album-related messages is written to session.log
+write_log('session', 'basic', 'Loading ' + PLUGIN_NAME)
+
+# SET UI COLUMNS FOR PICARD RHS
+if config.setting['ce_show_ui_tags'] and config.setting['ce_ui_tags']:
+ from picard.ui.itemviews import MainPanel
+ UI_TAGS = get_ui_tags().items()
+ for heading, tag_names in UI_TAGS:
+ heading_tag = '~' + heading + '_VAL'
+ MainPanel.columns.append((N_(heading), heading_tag))
+ write_log('session', 'info', 'UI_TAGS')
+ write_log('session', 'info', UI_TAGS)
+
+
+# set defaults for certain options that MUST be manually changed by the
+# user each time they are to be over-ridden
+config.setting['use_cache'] = True
+config.setting['ce_options_overwrite'] = False
+config.setting['track_ars'] = True
+config.setting['release_ars'] = True
+
+
+# REFERENCE DATA
+REF_DICT = get_references_from_file(
+ 'session',
+ config.setting['cwp_muso_path'],
+ config.setting['cwp_muso_refdb'])
+write_log('session', 'info', 'External references (Muso):')
+write_log('session', 'info', REF_DICT)
+COMPOSER_DICT = REF_DICT['composers']
+if config.setting['cwp_muso_classical'] and not COMPOSER_DICT:
+ write_log('session', 'error', 'No composer roster found')
+for cd in COMPOSER_DICT:
+ cd['lc_name'] = [c.lower() for c in cd['name']]
+ cd['lc_sort'] = [c.lower() for c in cd['sort']]
+PERIOD_DICT = REF_DICT['periods']
+if (config.setting['cwp_muso_dates']
+ or config.setting['cwp_muso_periods']) and not PERIOD_DICT:
+ write_log('session', 'error', 'No period map found')
+GENRE_DICT = REF_DICT['genres']
+if config.setting['cwp_muso_genres'] and not GENRE_DICT:
+ write_log('session', 'error', 'No classical genre list found')
+
+# API CALLS
+register_track_metadata_processor(PartLevels().add_work_info)
+register_track_metadata_processor(ExtraArtists().add_artist_info)
+register_options_page(ClassicalExtrasOptionsPage)
+
+# END
+write_log('session', 'basic', 'Finished intialisation')
diff --git a/plugins/classical_extras/const.py b/plugins/classical_extras/const.py
new file mode 100644
index 00000000..616c50f5
--- /dev/null
+++ b/plugins/classical_extras/const.py
@@ -0,0 +1,1148 @@
+# -*- coding: utf-8 -*-
+"""
+Declare constants for Picard Classical Extras plugin
+v2.0.2
+"""
+# Copyright (C) 2018 Mark Evens
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+
+RELATION_TYPES = {
+ 'work': [
+ 'arranger',
+ 'instrument arranger',
+ 'orchestrator',
+ 'composer',
+ 'writer',
+ 'lyricist',
+ 'librettist',
+ 'revised by',
+ 'translator',
+ 'reconstructed by',
+ 'vocal arranger'],
+ 'release': [
+ 'instrument',
+ 'performer',
+ 'vocal',
+ 'performing orchestra',
+ 'conductor',
+ 'chorus master',
+ 'concertmaster',
+ 'arranger',
+ 'instrument arranger',
+ 'orchestrator',
+ 'vocal arranger'],
+ 'recording': [
+ 'instrument',
+ 'performer',
+ 'vocal',
+ 'performing orchestra',
+ 'conductor',
+ 'chorus master',
+ 'concertmaster',
+ 'arranger',
+ 'instrument arranger',
+ 'orchestrator',
+ 'vocal arranger']}
+
+ARTISTS_OPTIONS = [
+ {'option': 'classical_extra_artists',
+ 'name': 'run extra artists',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cea_orchestras',
+ 'name': 'orchestra strings',
+ 'type': 'Text',
+ 'default': 'orchestra, philharmonic, philharmonica, philharmoniker, musicians, academy, symphony, orkester'
+ },
+ {'option': 'cea_choirs',
+ 'name': 'choir strings',
+ 'type': 'Text',
+ 'default': 'choir, choir vocals, chorus, singers, domchors, domspatzen, koor, kammerkoor'
+ },
+ {'option': 'cea_groups',
+ 'name': 'group strings',
+ 'type': 'Text',
+ 'default': 'ensemble, band, group, trio, quartet, quintet, sextet, septet, octet, chamber, consort, players, '
+ 'les ,the , quartett'
+ },
+ {'option': 'cea_aliases',
+ 'name': 'replace artist name with alias?',
+ 'value': 'replace',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cea_aliases_composer',
+ 'name': 'replace artist name with alias?',
+ 'value': 'composer',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'cea_no_aliases',
+ 'name': 'replace artist name with alias?',
+ 'value': 'no replace',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'cea_alias_overrides',
+ 'name': 'alias vs credited-as',
+ 'value': 'alias over-rides',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cea_credited_overrides',
+ 'name': 'alias vs credited-as',
+ 'value': 'credited-as over-rides',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'cea_ra_use',
+ 'name': 'use recording artist',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'cea_ra_trackartist',
+ 'name': 'recording artist name style',
+ 'value': 'track artist',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'cea_ra_performer',
+ 'name': 'recording artist name style',
+ 'value': 'performer',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cea_ra_replace_ta',
+ 'name': 'recording artist effect on track artist',
+ 'value': 'replace',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'cea_ra_noblank_ta',
+ 'name': 'disallow blank recording artist',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'cea_ra_merge_ta',
+ 'name': 'recording artist effect on track artist',
+ 'value': 'merge',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cea_composer_album',
+ 'name': 'Album prefix',
+ # 'value': 'Composer', # Can't use 'value' if there is only one option, otherwise False will revert to default
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cea_arrangers',
+ 'name': 'include arrangers',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cea_no_lyricists',
+ 'name': 'exclude lyricists if no vocals',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cea_cyrillic',
+ 'name': 'fix cyrillic',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ # {'option': 'cea_genres',
+ # 'name': 'infer work types',
+ # 'type': 'Boolean',
+ # 'default': True
+ # },
+ # Note that the above is no longer used - replaced by cwp_genres_infer from v0.9.2
+ {'option': 'cea_credited',
+ 'name': 'use release credited-as name',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cea_release_relationship_credited',
+ 'name': 'use release relationship credited-as name',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cea_group_credited',
+ 'name': 'use release-group credited-as name',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cea_recording_credited',
+ 'name': 'use recording credited-as name',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'cea_recording_relationship_credited',
+ 'name': 'use recording relationship credited-as name',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cea_track_credited',
+ 'name': 'use track credited-as name',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cea_performer_credited',
+ 'name': 'use credited-as name for performer',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cea_composer_credited',
+ 'name': 'use credited-as name for composer',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'cea_inst_credit',
+ 'name': 'use credited instrument',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cea_no_solo',
+ 'name': 'exclude solo',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cea_chorusmaster',
+ 'name': 'chorusmaster',
+ 'type': 'Text',
+ 'default': 'choirmaster'
+ },
+ {'option': 'cea_orchestrator',
+ 'name': 'orchestrator',
+ 'type': 'Text',
+ 'default': 'orch.'
+ },
+ {'option': 'cea_concertmaster',
+ 'name': 'concertmaster',
+ 'type': 'Text',
+ 'default': 'leader'
+ },
+ {'option': 'cea_lyricist',
+ 'name': 'lyricist',
+ 'type': 'Text',
+ 'default': 'lyrics'
+ },
+ {'option': 'cea_librettist',
+ 'name': 'librettist',
+ 'type': 'Text',
+ 'default': 'libretto'
+ },
+ {'option': 'cea_writer',
+ 'name': 'writer',
+ 'type': 'Text',
+ 'default': 'writer'
+ },
+ {'option': 'cea_arranger',
+ 'name': 'arranger',
+ 'type': 'Text',
+ 'default': 'arr.'
+ },
+ {'option': 'cea_reconstructed',
+ 'name': 'reconstructed by',
+ 'type': 'Text',
+ 'default': 'reconstructed'
+ },
+ {'option': 'cea_revised',
+ 'name': 'revised by',
+ 'type': 'Text',
+ 'default': 'revised'
+ },
+ {'option': 'cea_translator',
+ 'name': 'translator',
+ 'type': 'Text',
+ 'default': 'trans.'
+ },
+ {'option': 'cea_split_lyrics',
+ 'name': 'split lyrics',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cea_lyrics_tag',
+ 'name': 'lyrics',
+ 'type': 'Text',
+ 'default': 'lyrics'
+ },
+ {'option': 'cea_album_lyrics',
+ 'name': 'album lyrics',
+ 'type': 'Text',
+ 'default': 'albumnotes'
+ },
+ {'option': 'cea_track_lyrics',
+ 'name': 'track lyrics',
+ 'type': 'Text',
+ 'default': 'tracknotes'
+ }
+]
+
+TAG_OPTIONS = [
+ {'option': 'cea_blank_tag',
+ 'name': 'Tags to blank',
+ 'type': 'Text',
+ 'default': 'artist, artistsort'
+ },
+ {'option': 'cea_blank_tag_2',
+ 'name': 'Tags to blank 2',
+ 'type': 'Text',
+ 'default': 'performer:orchestra, performer:choir, performer:choir vocals'
+ },
+ {'option': 'cea_keep',
+ 'name': 'File tags to keep',
+ 'type': 'Text',
+ 'default': ''
+ },
+ {'option': 'cea_clear_tags',
+ 'name': 'Clear previous tags',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'cea_tag_sort',
+ 'name': 'populate sort tags',
+ 'type': 'Boolean',
+ 'default': True
+ }
+]
+# (tag mapping detail lines)
+default_list = [
+ ('album_soloists, album_ensembles, album_conductors', 'artist, artists', False),
+ ('recording_artists', 'artist, artists', True),
+ ('soloist_names, ensemble_names, conductors', 'artist, artists', True),
+ ('soloists', 'soloists, trackartist, involved people', False),
+ ('release', 'release_name', False),
+ ('ensemble_names', 'band', False),
+ ('composers', 'artist', True),
+ ('MB_artists', 'composer', True),
+ ('arranger', 'composer', True)
+]
+TAG_DETAIL_OPTIONS = []
+for i in range(0, 16):
+ if i < len(default_list):
+ default_source, default_tag, default_cond = default_list[i]
+ else:
+ default_source = ''
+ default_tag = ''
+ default_cond = False
+ TAG_DETAIL_OPTIONS.append({'option': 'cea_source_' + str(i + 1),
+ 'name': 'line ' + str(i + 1) + '_source',
+ 'type': 'Combo',
+ 'default': default_source
+ })
+ TAG_DETAIL_OPTIONS.append({'option': 'cea_tag_' + str(i + 1),
+ 'name': 'line ' + str(i + 1) + '_tag',
+ 'type': 'Text',
+ 'default': default_tag
+ })
+ TAG_DETAIL_OPTIONS.append({'option': 'cea_cond_' + str(i + 1),
+ 'name': 'line ' + str(i + 1) + '_conditional',
+ 'type': 'Boolean',
+ 'default': default_cond
+ })
+
+WORKPARTS_OPTIONS = [
+ {'option': 'classical_work_parts',
+ 'name': 'run work parts',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cwp_collections',
+ 'name': 'include collection relations',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cwp_allow_empty_parts',
+ # allow parts to be blank if there is arrangement or partial text label
+ # checked = split
+ 'name': 'allow-empty-parts',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cwp_common_chars',
+ # for use in strip_parent_from_work
+ # where no match exists and substring elimination is used
+ # this sets the minimum number of matching 'words' (words followed by punctuation/spaces or EOL)
+ # required before they will be eliminated
+ # 0 => no elimination
+ # default is 2 words
+ 'name': 'min common words to eliminate',
+ 'type': 'Integer',
+ 'default': 2
+ },
+ {'option': 'cwp_proximity',
+ # proximity of new words in title comparison which will result in
+ # infill words being included as well. 2 means 2-word 'gaps' of
+ # existing words between new words will be treated as 'new'
+ 'name': 'in-string proximity trigger',
+ 'type': 'Integer',
+ 'default': 2
+ },
+ {'option': 'cwp_end_proximity',
+ # proximity measure to be used when infilling to the end of the title
+ 'name': 'end-string proximity trigger',
+ 'type': 'Integer',
+ 'default': 1
+ },
+ {'option': 'cwp_split_hyphenated',
+ # splitting of hyphenated words for matching purposes
+ # checked = split
+ 'name': 'hyphen-splitting',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cwp_substring_match',
+ # Proportion of a string to be matched to a (usually larger) string for
+ # it to be considered essentially similar
+ 'name': 'similarity threshold',
+ 'type': 'Integer',
+ 'default': 100
+ },
+ {'option': 'cwp_fill_part',
+ # Fill part name with title text if it would otherwise
+ # have no text other than arrangement or partial annotations
+ 'name': 'disallow empty part names',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cwp_prepositions',
+ 'name': 'prepositions',
+ 'type': 'Text',
+ 'default': "a, the, in, on, at, of, after, and, de, d'un, d'une, la, le, no, from, &, e, ed, et, un,"
+ " une, al, ala, alla"
+ },
+ {'option': 'cwp_removewords',
+ 'name': 'ignore prefixes',
+ 'type': 'Text',
+ 'default': ' part , act , scene, movement, movt, no. , no , n., n , nr., nr , book , the , a , la , le , un ,'
+ ' une , el , il , tableau, from , KV ,Concerto in, Concerto'
+ },
+ {'option': 'cwp_synonyms',
+ 'name': 'synonyms',
+ 'type': 'PlainText',
+ 'default': '(1, one) / (2, two) / (3, three) / (&, and) / (Rezitativ, Recitativo, Recitative) / '
+ '(Sinfonia, Sinfonie, Symphonie, Symphony) / (Arie, Aria) / '
+ '(Minuetto, Menuetto, Minuetta, Menuet, Minuet) / (Bourée, Bouree , Bourrée)'
+ },
+ {'option': 'cwp_replacements',
+ 'name': 'replacements',
+ 'type': 'Text',
+ 'default': '(words to be replaced, replacement words) / (please blank me, ) / (etc, etc)'
+ },
+ {'option': 'cwp_titles',
+ 'name': 'Style',
+ 'value': 'Titles',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'cwp_works',
+ 'name': 'Style',
+ 'value': 'Works',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'cwp_extended',
+ 'name': 'Style',
+ 'value': 'Extended',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cwp_hierarchical_works',
+ 'name': 'Work source',
+ 'value': 'Hierarchy',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cwp_level0_works',
+ 'name': 'Work source',
+ 'value': 'Level_0',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'cwp_derive_works_from_title',
+ 'name': 'Derive works from title',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cwp_movt_tag_inc',
+ 'name': 'movement tag inc num',
+ 'type': 'Text',
+ 'default': 'part, movement, subtitle'
+ },
+ {'option': 'cwp_movt_tag_exc',
+ 'name': 'movement tag exc num',
+ 'type': 'Text',
+ 'default': ''
+ },
+ {'option': 'cwp_movt_tag_inc1',
+ 'name': '1-level movement tag inc num',
+ 'type': 'Text',
+ 'default': 'movement'
+ },
+ {'option': 'cwp_movt_tag_exc1',
+ 'name': '1-level movement tag exc num',
+ 'type': 'Text',
+ 'default': ''
+ },
+ {'option': 'cwp_movt_no_tag',
+ 'name': 'movement num tag',
+ 'type': 'Text',
+ 'default': 'movementnumber'
+ },
+ {'option': 'cwp_movt_tot_tag',
+ 'name': 'movement tot tag',
+ 'type': 'Text',
+ 'default': 'movementtotal'
+ },
+ {'option': 'cwp_work_tag_multi',
+ 'name': 'multi-level work tag',
+ 'type': 'Text',
+ 'default': 'groupheading, work'
+ },
+ {'option': 'cwp_work_tag_single',
+ 'name': 'single level work tag',
+ 'type': 'Text',
+ 'default': ''
+ },
+ {'option': 'cwp_top_tag',
+ 'name': 'top level work tag',
+ 'type': 'Text',
+ 'default': 'top_work, style, grouping'
+ },
+ {'option': 'cwp_multi_work_sep',
+ 'name': 'multi-level work separator',
+ 'type': 'Combo',
+ 'default': ':'
+ },
+ {'option': 'cwp_single_work_sep',
+ 'name': 'single level work separator',
+ 'type': 'Combo',
+ 'default': ':'
+ },
+ {'option': 'cwp_movt_no_sep',
+ 'name': 'movement number separator',
+ 'type': 'Combo',
+ 'default': '.'
+ },
+ {'option': 'cwp_partial',
+ 'name': 'show partial recordings',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cwp_partial_text',
+ 'name': 'partial text',
+ 'type': 'Text',
+ 'default': '(part)'
+ },
+ {'option': 'cwp_arrangements',
+ 'name': 'include arrangement of',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cwp_arrangements_text',
+ 'name': 'arrangements text',
+ 'type': 'Text',
+ 'default': 'Arrangement:'
+ },
+ {'option': 'cwp_medley',
+ 'name': 'list medleys',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cwp_medley_text',
+ 'name': 'medley text',
+ 'type': 'Text',
+ 'default': 'Medley'
+ }
+]
+# Options on "Genres etc." tab
+
+GENRE_OPTIONS = [
+ {'option': 'cwp_genre_tag',
+ 'name': 'main genre tag',
+ 'type': 'Text',
+ 'default': 'genre'
+ },
+ {'option': 'cwp_subgenre_tag',
+ 'name': 'sub-genre tag',
+ 'type': 'Text',
+ 'default': 'sub-genre'
+ },
+ {'option': 'cwp_genres_use_file',
+ 'name': 'source genre from file',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cwp_genres_use_folks',
+ 'name': 'source genre from folksonomy tags',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cwp_genres_use_worktype',
+ 'name': 'source genre from work-type(s)',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cwp_genres_infer',
+ 'name': 'infer genre from artist details(s)',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ # Note that the "infer from artists" option was in the "artists"
+ # section - legacy from v0.9.1 & prior
+ {'option': 'cwp_genres_filter',
+ 'name': 'apply filter to genres',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cwp_genres_classical_main',
+ 'name': 'classical main genres',
+ 'type': 'PlainText',
+ 'default': 'Classical, Chamber music, Concerto, Symphony, Opera, Orchestral, Sonata, Choral, Aria, Ballet, '
+ 'Oratorio, Motet, Symphonic poem, Suite, Partita, Song-cycle, Overture, '
+ 'Mass, Cantata'
+ },
+ {'option': 'cwp_genres_classical_sub',
+ 'name': 'classical sub-genres',
+ 'type': 'PlainText',
+ 'default': 'Chant, Classical crossover, Minimalism, Avant-garde, Impressionist, Aria, Duet, Trio, Quartet'
+ },
+ {'option': 'cwp_genres_other_main',
+ 'name': 'general main genres',
+ 'type': 'PlainText',
+ 'default': 'Alternative music, Blues, Country, Dance, Easy listening, Electronic music, Folk, Folk / pop, '
+ 'Hip hop / rap, Indie, Religious, Asian, Jazz, Latin, New age, Pop, R&B / Soul, Reggae, Rock, '
+ 'World music, Celtic folk, French Medieval'
+ },
+ {'option': 'cwp_genres_other_sub',
+ 'name': 'general sub-genres',
+ 'type': 'PlainText',
+ 'default': 'Song, Vocal, Christmas, Instrumental'
+ },
+ {'option': 'cwp_genres_arranger_as_composer',
+ 'name': 'treat arranger as for composer for genre-setting',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cwp_genres_classical_all',
+ 'name': 'make tracks classical',
+ 'value': 'all',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'cwp_genres_classical_selective',
+ 'name': 'make tracks classical',
+ 'value': 'selective',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cwp_genres_classical_exclude',
+ 'name': 'exclude "classical" from main genre tag',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'cwp_genres_flag_text',
+ 'name': 'classical flag',
+ 'type': 'Text',
+ 'default': '1'
+ },
+ {'option': 'cwp_genres_flag_tag',
+ 'name': 'classical flag tag',
+ 'type': 'Text',
+ 'default': 'is_classical'
+ },
+ {'option': 'cwp_genres_default',
+ 'name': 'default genre',
+ 'type': 'Text',
+ 'default': 'Other'
+ },
+ {'option': 'cwp_instruments_tag',
+ 'name': 'instruments tag',
+ 'type': 'Text',
+ 'default': 'instrument'
+ },
+ {'option': 'cwp_instruments_MB_names',
+ 'name': 'use MB instrument names',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cwp_instruments_credited_names',
+ 'name': 'use credited instrument names',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cwp_key_tag',
+ 'name': 'key tag',
+ 'type': 'Text',
+ 'default': 'key'
+ },
+ {'option': 'cwp_key_contingent_include',
+ 'name': 'contingent include key in workname',
+ 'value': 'contingent',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cwp_key_never_include',
+ 'name': 'never include key in workname',
+ 'value': 'never',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'cwp_key_include',
+ 'name': 'include key in workname',
+ 'value': 'always',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'cwp_workdate_tag',
+ 'name': 'workdate tag',
+ 'type': 'Text',
+ 'default': 'work_year'
+ },
+ {'option': 'cwp_workdate_source_composed',
+ 'name': 'use composed for workdate',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cwp_workdate_source_published',
+ 'name': 'use published for workdate',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cwp_workdate_source_premiered',
+ 'name': 'use premiered for workdate',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cwp_workdate_use_first',
+ 'name': 'use workdate sources sequentially',
+ 'value': 'sequence',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'cwp_workdate_use_all',
+ 'name': 'use all workdate sources',
+ 'value': 'all',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cwp_workdate_annotate',
+ 'name': 'annotate dates',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cwp_workdate_include',
+ 'name': 'include workdate in workname',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cwp_period_tag',
+ 'name': 'period tag',
+ 'type': 'Text',
+ 'default': 'period'
+ },
+ {'option': 'cwp_periods_arranger_as_composer',
+ 'name': 'treat arranger as for composer for period-setting',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'cwp_period_map',
+ 'name': 'period map',
+ 'type': 'PlainText',
+ 'default': 'Early, -3000,800; Medieval, 800,1400; Renaissance, 1400, 1600; Baroque, 1600,1750; '
+ 'Classical, 1750,1820; Early Romantic, 1800,1850; Late Romantic, 1850,1910; '
+ '20th Century, 1910,1975; Contemporary, 1975,2525'
+ }
+]
+# Picard options which are also saved (NB only affects plugin processing - not main Picard processing)
+PICARD_OPTIONS = [
+ {'option': 'standardize_artists',
+ 'name': 'standardize artists',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'translate_artist_names',
+ 'name': 'translate artist names',
+ 'type': 'Boolean',
+ 'default': True
+ },
+]
+
+# other options (not saved in file tags)
+OTHER_OPTIONS = [
+ {'option': 'use_cache',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cwp_aliases',
+ 'name': 'replace with alias?',
+ 'value': 'replace',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cwp_no_aliases',
+ 'name': 'replace with alias?',
+ 'value': 'no replace',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'cwp_aliases_all',
+ 'name': 'alias replacement type',
+ 'value': 'all',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'cwp_aliases_greek',
+ 'name': 'alias replacement type',
+ 'value': 'non-latin',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cwp_aliases_tagged',
+ 'name': 'alias replacement type',
+ 'value': 'tagged works',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'cwp_aliases_tag_text',
+ 'name': 'use_alias tag text',
+ 'type': 'Text',
+ 'default': 'use_alias'
+ },
+ {'option': 'cwp_aliases_tags_all',
+ 'name': 'use_alias tags all',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'cwp_aliases_tags_user',
+ 'name': 'use_alias tags user',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'cwp_use_sk',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'cwp_write_sk',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'cwp_retries',
+ 'type': 'Integer',
+ 'default': 6
+ },
+ {'option': 'cwp_use_muso_refdb',
+ 'name': 'use Muso ref database',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'cwp_muso_genres',
+ 'name': 'use Muso classical genres',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'cwp_muso_classical',
+ 'name': 'use Muso classical composers',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'cwp_muso_dates',
+ 'name': 'use Muso composer dates',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'cwp_muso_periods',
+ 'name': 'use Muso periods',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'cwp_muso_path',
+ 'name': 'path to Muso database',
+ 'type': 'Text',
+ 'default': 'C:\\Users\\Public\\Music\\muso\\database'
+ },
+ {'option': 'cwp_muso_refdb',
+ 'name': 'name of Muso reference database',
+ 'type': 'Text',
+ 'default': 'Reference.xml'
+ },
+ {'option': 'log_error',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'log_warning',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'log_debug',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'log_basic',
+ 'type': 'Boolean',
+ 'default': True
+ },
+ {'option': 'log_info',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'ce_version_tag',
+ 'type': 'Text',
+ 'default': 'stamp'
+ },
+ {'option': 'cea_options_tag',
+ 'type': 'Text',
+ 'default': 'comment'
+ },
+ {'option': 'cwp_options_tag',
+ 'type': 'Text',
+ 'default': 'comment'
+ },
+ {'option': 'cea_override',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'ce_tagmap_override',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'cwp_override',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'ce_genres_override',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'ce_options_overwrite',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'ce_no_run',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'ce_show_ui_tags',
+ 'type': 'Boolean',
+ 'default': False
+ },
+ {'option': 'ce_ui_tags',
+ ## Note that this is not just for work parts (although that is the main use),
+ # but cwp prefix is to make use of code for synonyms
+ 'name': 'tags for ui columns',
+ 'type': 'PlainText',
+ 'default': 'Work diff: (groupheading_DIFF, work_DIFF, top_work_DIFF, grouping_DIFF) / Part diff: (part_DIFF, movement_DIFF) / Missing file metadata: 002_important_warning'
+ }
+]
+
+ARTIST_TYPE_ORDER = {'vocal': 1,
+ 'instrument': 1,
+ 'performer': 0,
+ 'performing orchestra': 2,
+ 'concertmaster': 3,
+ 'conductor': 4,
+ 'chorus master': 5,
+ 'composer': 6,
+ 'writer': 7,
+ 'reconstructed by': 8,
+ 'instrument arranger': 9,
+ 'vocal arranger': 9,
+ 'arranger': 11,
+ 'orchestrator': 12,
+ 'revised by': 13,
+ 'lyricist': 14,
+ 'librettist': 15,
+ 'translator': 16
+ }
+
+CYRILLIC_UPPER = {
+ u'А': u'A',
+ u'Б': u'B',
+ u'В': u'V',
+ u'Г': u'G',
+ u'Д': u'D',
+ u'Е': u'E',
+ u'Ё': u'E',
+ u'Ж': u'Zh',
+ u'З': u'Z',
+ u'И': u'I',
+ u'Й': u'Y',
+ u'К': u'K',
+ u'Л': u'L',
+ u'М': u'M',
+ u'Н': u'N',
+ u'О': u'O',
+ u'П': u'P',
+ u'Р': u'R',
+ u'С': u'S',
+ u'Т': u'T',
+ u'У': u'U',
+ u'Ф': u'F',
+ u'Х': u'H',
+ u'Ц': u'Ts',
+ u'Ч': u'Ch',
+ u'Ш': u'Sh',
+ u'Щ': u'Sch',
+ u'Ъ': u'',
+ u'Ы': u'Y',
+ u'Ь': u'',
+ u'Э': u'E',
+ u'Ю': u'Yu',
+ u'Я': u'Ya'
+ }
+CYRILLIC_LOWER = {
+ u'а': u'a',
+ u'б': u'b',
+ u'в': u'v',
+ u'г': u'g',
+ u'д': u'd',
+ u'е': u'e',
+ u'ё': u'e',
+ u'ж': u'zh',
+ u'з': u'z',
+ u'и': u'i',
+ u'й': u'y',
+ u'к': u'k',
+ u'л': u'l',
+ u'м': u'm',
+ u'н': u'n',
+ u'о': u'o',
+ u'п': u'p',
+ u'р': u'r',
+ u'с': u's',
+ u'т': u't',
+ u'у': u'u',
+ u'ф': u'f',
+ u'х': u'h',
+ u'ц': u'ts',
+ u'ч': u'ch',
+ u'ш': u'sh',
+ u'щ': u'sch',
+ u'ъ': u'',
+ u'ы': u'y',
+ u'ь': u'',
+ u'э': u'e',
+ u'ю': u'yu',
+ u'я': u'ya'
+}
+
+def tag_strings(pre):
+ TAG_STRINGS = {
+ 'writer': (
+ 'composer',
+ pre + '_writers',
+ 'composersort',
+ pre + '_writers_sort'),
+ 'composer': (
+ 'composer',
+ pre + '_composers',
+ 'composersort',
+ pre + '_composers_sort'),
+ 'lyricist': (
+ 'lyricist',
+ pre + '_lyricists',
+ '~lyricists_sort',
+ pre + '_lyricists_sort'),
+ 'librettist': (
+ 'lyricist',
+ pre + '_librettists',
+ '~lyricists_sort',
+ pre + '_librettists_sort'),
+ 'revised by': (
+ 'arranger',
+ pre + '_revisors',
+ '~arranger_sort',
+ pre + '_revisors_sort'),
+ 'translator': (
+ 'lyricist',
+ pre + '_translators',
+ '~lyricists_sort',
+ pre + '_translators_sort'),
+ 'reconstructed by': (
+ 'arranger',
+ pre + '_reconstructors',
+ '~arranger_sort',
+ pre + '_reconstructors_sort'),
+ 'arranger': (
+ 'arranger',
+ pre + '_arrangers',
+ '~arranger_sort',
+ pre + '_arrangers_sort'),
+ 'instrument arranger': (
+ 'arranger',
+ pre + '_arrangers',
+ '~arranger_sort',
+ pre + '_arrangers_sort'),
+ 'orchestrator': (
+ 'arranger',
+ pre + '_orchestrators',
+ '~arranger_sort',
+ pre + '_orchestrators_sort'),
+ 'vocal arranger': (
+ 'arranger',
+ pre + '_arrangers',
+ '~arranger_sort',
+ pre + '_arrangers_sort'),
+ 'performer': (
+ 'performer:',
+ pre + '_performers',
+ '~performer_sort',
+ pre + '_performers_sort'),
+ 'instrument': (
+ 'performer:',
+ pre + '_performers',
+ '~performer_sort',
+ pre + '_performers_sort'),
+ 'vocal': (
+ 'performer:',
+ pre + '_performers',
+ '~performer_sort',
+ pre + '_performers_sort'),
+ 'performing orchestra': (
+ 'performer:orchestra',
+ pre + '_ensembles',
+ '~performer_sort',
+ pre + '_ensembles_sort'),
+ 'conductor': (
+ 'conductor',
+ pre + '_conductors',
+ '~conductor_sort',
+ pre + '_conductors_sort'),
+ 'chorus master': (
+ 'conductor',
+ pre + '_chorusmasters',
+ '~conductor_sort',
+ pre + '_chorusmasters_sort'),
+ 'concertmaster': (
+ 'performer',
+ pre + '~_leaders',
+ '~performer_sort',
+ pre + '_leaders_sort')}
+ return TAG_STRINGS
+
+INSERTIONS = ['writer',
+ 'lyricist',
+ 'librettist',
+ 'revised by',
+ 'translator',
+ 'arranger',
+ 'reconstructed by',
+ 'orchestrator',
+ 'instrument arranger',
+ 'vocal arranger',
+ 'chorus master']
\ No newline at end of file
diff --git a/plugins/classical_extras/options_classical_extras.ui b/plugins/classical_extras/options_classical_extras.ui
new file mode 100644
index 00000000..02a16cd3
--- /dev/null
+++ b/plugins/classical_extras/options_classical_extras.ui
@@ -0,0 +1,11987 @@
+
+
should be selected otherwise this section will not run
")) + self.use_cea.setText(_translate("ClassicalExtrasOptionsPage", "Create extra artist metadata (MUST BE TICKED FOR THIS SECTION TO RUN)")) + self.infer_worktypes_old_label.setText(_translate("ClassicalExtrasOptionsPage", "(Note that the \"infer work-types\" option has moved to the \"genres\" tab)")) + self.naming_style_note_label.setText(_translate("ClassicalExtrasOptionsPage", "The naming style for \'artist\' tags is set in the main Picard Options->Metadata section
")) + self.naming_options_label.setText(_translate("ClassicalExtrasOptionsPage", "Work-artist / performer naming options
")) + self.naming_options_frame.setWhatsThis(_translate("ClassicalExtrasOptionsPage", ""Work-artists" are types such as composer, writer, arranger and lyricist who belong to the MusicBrainz Work-Artist relationship
"Performers" are types such as performer and conductor who belong to the MusicBrainz Recording-Artist relationship
")) + self.label_22.setText(_translate("ClassicalExtrasOptionsPage", "This section does not change the contents of "artist" or "album artist" tags - it only affects writer (composer etc.) and peformer tags, by using as-credited/alias names from the artist data for the release.
")) + self.credited_as_options_box.setToolTip(_translate("ClassicalExtrasOptionsPage", "Select the source for \'as-credited\' names - whether these are applied depends on the sub-options choices.
")) + self.names_to_use_box.setTitle(_translate("ClassicalExtrasOptionsPage", "Names to use...")) + self.cea_recording_credited.setText(_translate("ClassicalExtrasOptionsPage", "Use \"credited as\" name for work-artists/performers who are recording artists")) + self.cea_group_credited.setText(_translate("ClassicalExtrasOptionsPage", "and/or release group artists")) + self.cea_credited.setToolTip(_translate("ClassicalExtrasOptionsPage", "Select the tag types where any \'as-credited\' names will be applied - whether these are applied depends on the sub-options choices.
")) + self.places_to_use_them_box.setTitle(_translate("ClassicalExtrasOptionsPage", "Places to use them ...")) + self.cea_performer_credited.setText(_translate("ClassicalExtrasOptionsPage", "Use for performing artists")) + self.cea_composer_credited.setText(_translate("ClassicalExtrasOptionsPage", "Use for work-artists")) + self.naming_sub_options_box.setTitle(_translate("ClassicalExtrasOptionsPage", "Sub-options")) + self.cea_alias_overrides.setToolTip(_translate("ClassicalExtrasOptionsPage", "Alias (if it exists) will over-ride as-credited
")) + self.cea_alias_overrides.setText(_translate("ClassicalExtrasOptionsPage", "Alias over-rides credited-as")) + self.cea_credited_overrides.setToolTip(_translate("ClassicalExtrasOptionsPage", "As-credited (if it exists) will over-ride alias
")) + self.cea_credited_overrides.setText(_translate("ClassicalExtrasOptionsPage", "Credited-as over-rides MB/Alias")) + self.cea_cyrillic.setToolTip(_translate("ClassicalExtrasOptionsPage", "Will be based on sort names. For cyrillic script names, patronyms will be removed.
")) + self.cea_cyrillic.setText(_translate("ClassicalExtrasOptionsPage", "Fix non-Latin text in names (where possible and if not fixed by other naming options)")) + self.MB_std_names_aliases_box_outer.setTitle(_translate("ClassicalExtrasOptionsPage", "MusicBrainz standard names and Aliases")) + self.cea_no_aliases.setToolTip(_translate("ClassicalExtrasOptionsPage", "Do not use aliases (but may be replaced by as-credited name)
")) + self.cea_no_aliases.setText(_translate("ClassicalExtrasOptionsPage", "Use MB standard names")) + self.cea_aliases.setToolTip(_translate("ClassicalExtrasOptionsPage", "Alias will only be available for use if the work-artist/performer is also a release artist, recording artist or track artist.
")) + self.cea_aliases.setText(_translate("ClassicalExtrasOptionsPage", "Use alias for all work-artists/performers")) + self.cea_aliases_composer.setToolTip(_translate("ClassicalExtrasOptionsPage", "Only use alias (if available) for work-artists (writers, composers, arrangers, lyricists etc.)
")) + self.cea_aliases_composer.setText(_translate("ClassicalExtrasOptionsPage", "Use alias for work-artists only")) + self.recording_artist_options_label.setText(_translate("ClassicalExtrasOptionsPage", "Recording artist options
")) + self.recording_artists_options_box.setToolTip(_translate("ClassicalExtrasOptionsPage", "Select recording artist options (see "What\'s this")
")) + self.recording_artists_options_box.setWhatsThis(_translate("ClassicalExtrasOptionsPage", "In MusicBrainz, the recording artist may be different from the track artist. For classical music, the MusicBrainz guidelines state that the track artist should be the composer; however the recording artist(s) is/are usually the principal performer(s).
Classical Extras puts the recording artists into \'hidden variables\' (as a minimum) using the chosen naming convention.
There is also option to allow you to replace the track artist by the recording artist (or to merge them). The chosen action will be applied to the \'artist\', \'artists\', \'artistsort\' and \'artists_sort\' tags. Note that \'artist\' is a single-valued string whereas \'artists\' is a list and may be multi-valued. Lists are properly merged, but because the \'artist\' string may have different join-phrases etc, a merged tag may have the recording artist(s) in brackets after the track artist(s).
")) + self.naming_convention_box.setWhatsThis(_translate("ClassicalExtrasOptionsPage", "In classical music (in MusicBrainz), recording artists will usually be performers whereas track artists are composers. By default, the naming convention for performers (set in the previous section) will be used (although only the as-credited name set for the recording artist will be applied). Alternatively, the naming convention for track artists can be used - which is determined by the main Picard metadat options.
")) + self.naming_convention_box.setTitle(_translate("ClassicalExtrasOptionsPage", "Naming convention as for ...")) + self.cea_ra_trackartist.setText(_translate("ClassicalExtrasOptionsPage", "...track artist (set in Picard options)")) + self.cea_ra_performer.setText(_translate("ClassicalExtrasOptionsPage", "... perfomers (set above)")) + self.cea_ra_use.setText(_translate("ClassicalExtrasOptionsPage", "Use recording artists to update track artists ->")) + self.ra_replace_merge_options_box.setTitle(_translate("ClassicalExtrasOptionsPage", "Replace/merge options")) + self.cea_ra_replace_ta.setText(_translate("ClassicalExtrasOptionsPage", "Replace track artist by recording artist")) + self.cea_ra_noblank_ta.setText(_translate("ClassicalExtrasOptionsPage", "Only replace if rec. artist exists")) + self.cea_ra_merge_ta.setText(_translate("ClassicalExtrasOptionsPage", "Merge track artist and recording artist")) + self.other_artist_options_label.setText(_translate("ClassicalExtrasOptionsPage", "Other artist options
")) + self.annotations_lh_box.setToolTip(_translate("ClassicalExtrasOptionsPage", "Enter text to appear in annotations. Do not use any quotation marks.
")) + self.annotations_lh_box.setTitle(_translate("ClassicalExtrasOptionsPage", "Annotations - performers and lyricists")) + self.label_44.setToolTip(_translate("ClassicalExtrasOptionsPage", "Annotation to include with "chorus master" in conductor tag.
")) + self.label_44.setText(_translate("ClassicalExtrasOptionsPage", "Chorus Master")) + self.label_46.setToolTip(_translate("ClassicalExtrasOptionsPage", "Annotation to include for "concert master" in performer tag.
")) + self.label_46.setText(_translate("ClassicalExtrasOptionsPage", "Concert Master")) + self.label_34.setToolTip(_translate("ClassicalExtrasOptionsPage", "Annotation for lyricist, to include in lyricist tag
")) + self.label_34.setText(_translate("ClassicalExtrasOptionsPage", "Lyricist")) + self.label_26.setToolTip(_translate("ClassicalExtrasOptionsPage", "Annotation for librettist, to include in lyricist tag
")) + self.label_26.setText(_translate("ClassicalExtrasOptionsPage", "Librettist")) + self.label_30.setToolTip(_translate("ClassicalExtrasOptionsPage", "Annotation for translator, to include in lyricist tag
")) + self.label_30.setText(_translate("ClassicalExtrasOptionsPage", "Translator")) + self.other_artist_checkboxes_box.setToolTip(_translate("ClassicalExtrasOptionsPage", "Select as required. See "What\'s this" for details.
")) + self.cea_arrangers.setToolTip(_translate("ClassicalExtrasOptionsPage", "This will gather together, for example, any arranger-type information from the recording, work or parent works and place it in the "arranger" tag (\'host\' tag), with the annotation (see details to right of this box) in brackets. All arranger types will also be put in a hidden variable, e.g. _cwp_orchestrators. The table below shows the artist types, host tag and hidden variable for each artist type.
| Artist type | Host tag | Hidden variable |
| ----------------- | ------------------| -------------------------------------- |
| writer | composer | writers |
| lyricist | lyricist | lyricists |
| librettist | lyricist | librettists |
| revised by | arranger | revisors |
| translator | lyricist | translators |
| arranger | arranger | arrangers |
| reconstructed by | arranger | reconstructors |
| orchestrator | arranger | orchestrators |
| instrument arranger | arranger | arrangers (with instrument type in brackets) |
| vocal arranger | arranger | arrangers (with voice type in brackets) |
| chorus master | conductor | chorusmasters |
| concertmaster | performer (with annotation as a sub-key) | leaders |
")) + self.cea_arrangers.setText(_translate("ClassicalExtrasOptionsPage", "Modify host tags and include annotations (see =>)")) + self.cea_composer_album.setToolTip(_translate("ClassicalExtrasOptionsPage", "This will add the composer(s) last name(s) before the album name, if they are listed as album artists. If there is more than one composer, they will be listed in the descending order of the length of their music on the release.
")) + self.cea_composer_album.setText(_translate("ClassicalExtrasOptionsPage", "Name album as \"Composer Last Name(s): Album Name\"")) + self.cea_no_lyricists.setWhatsThis(_translate("ClassicalExtrasOptionsPage", "This applies to both the Picard \'lyricist\' tag and the related internal plugin hidden variables \'_cwp_lyricists\' etc.
")) + self.cea_no_lyricists.setText(_translate("ClassicalExtrasOptionsPage", "Do not write \'lyricist\' tag if no vocal performers")) + self.cea_inst_credit.setText(_translate("ClassicalExtrasOptionsPage", "Use \"credited-as\" name for instrument")) + self.cea_no_solo.setToolTip(_translate("ClassicalExtrasOptionsPage", "Select to eliminate "additional", "solo" or "guest" from instrument description
")) + self.cea_no_solo.setWhatsThis(_translate("ClassicalExtrasOptionsPage", "MusicBrainz permits the use of "solo", "guest" and "additional" as instrument attributes although, for classical music, its use should be fairly rare - usually only if explicitly stated as a "solo" on the the sleevenotes. Classical Extras provides the option to exclude these attributes (the default), but you may wish to enable them for certain releases or non-Classical / cross-over releases.
")) + self.cea_no_solo.setText(_translate("ClassicalExtrasOptionsPage", "Do not include attributes (e.g. \'solo\') in an instrument type")) + self.annotations_rh_box.setToolTip(_translate("ClassicalExtrasOptionsPage", "Enter text to appear in annotations. Do not use any quotation marks.
")) + self.annotations_rh_box.setTitle(_translate("ClassicalExtrasOptionsPage", "Annotations - writers and arrangers")) + self.label_56.setText(_translate("ClassicalExtrasOptionsPage", "Writer")) + self.label_54.setText(_translate("ClassicalExtrasOptionsPage", "Arranger")) + self.label_45.setToolTip(_translate("ClassicalExtrasOptionsPage", "Text with which to annotate orchestrator in the arranger tag.
")) + self.label_45.setText(_translate("ClassicalExtrasOptionsPage", "Orchestrator")) + self.label_32.setToolTip(_translate("ClassicalExtrasOptionsPage", "Annotation for "reconstructed by", to include in arranger tag
")) + self.label_32.setText(_translate("ClassicalExtrasOptionsPage", "Reconstructed by")) + self.label_28.setToolTip(_translate("ClassicalExtrasOptionsPage", "Annotation for "revised by", to include in arranger tag tag
")) + self.label_28.setText(_translate("ClassicalExtrasOptionsPage", "Revised by")) + self.lyrics_label.setText(_translate("ClassicalExtrasOptionsPage", "Lyrics
")) + self.lyrics_box.setWhatsThis(_translate("ClassicalExtrasOptionsPage", "Please note that this section operates on the underlying input file tags, not the Picard-generated tags (MusicBrainz does not have lyrics)
Sometimes "lyrics" tags can contain album notes (repeated for every track in an album) as well as track notes and lyrics. This section will filter out the common text and place it in a different tag from the text which is unique to each track.
")) + self.cea_split_lyrics.setToolTip(_translate("ClassicalExtrasOptionsPage", "enables this section
")) + self.cea_split_lyrics.setText(_translate("ClassicalExtrasOptionsPage", "Split lyrics tag into track and album levels")) + self.lyrics_and_notes_tags_frame.setToolTip(_translate("ClassicalExtrasOptionsPage", "Enter a valid tag name (no spaces, punctuation or special charcaters other than underline; use lower case)
")) + self.label_50.setToolTip(_translate("ClassicalExtrasOptionsPage", "The name of the lyrics file tag in the input file (normally just \'lyrics\')
")) + self.label_50.setText(_translate("ClassicalExtrasOptionsPage", "Incoming lyrics tag (i.e. file tag)")) + self.label_51.setToolTip(_translate("ClassicalExtrasOptionsPage", "The name of the tag where common text should be placed
")) + self.label_51.setText(_translate("ClassicalExtrasOptionsPage", "Tag for album notes / lyrics")) + self.label_52.setToolTip(_translate("ClassicalExtrasOptionsPage", "The name of the tag where notes/lyrics unique to a track should be placed
")) + self.label_52.setText(_translate("ClassicalExtrasOptionsPage", "Tag for track notes / lyrics")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.Artists), _translate("ClassicalExtrasOptionsPage", "Artists")) + self.use_cwp.setToolTip(_translate("ClassicalExtrasOptionsPage", ""Include all work levels" should be selected otherwise this section will not run.
")) + self.use_cwp.setText(_translate("ClassicalExtrasOptionsPage", "Include all work levels (MUST BE TICKED FOR THIS SECTION TO RUN)*")) + self.cwp_collections.setToolTip(_translate("ClassicalExtrasOptionsPage", "This will include parent works where the relationship has the attribute \'part of collection\'.
PLEASE BE CONSISTENT and do not use different options on albums with the same works, or the results may be unexpected.
Select to use cached works. Deselect to refesh from MusicBrainz.
")) + self.use_cache.setWhatsThis(_translate("ClassicalExtrasOptionsPage", ""Use cache" prevents excessive look-ups of the MB database. Every look-up of a parent work needs to be performed separately (hopefully the MB database might make this easier some day). Network usage constraints by MB means that each look-up takes a minimum of 1 second. Once a release has been looked-up, the works are retained in cache, significantly reducing the time required if, say, the options are changed and the data refreshed. However, if the user edits the works in the MB database then the cache will need to be turned off temporarily for the refresh to find the new/changed works. Also some types of work (e.g. arrangements) will require a full look-up if options have been changed.
")) + self.use_cache.setText(_translate("ClassicalExtrasOptionsPage", "Use cache (if available)*")) + self.work_style_label.setText(_translate("ClassicalExtrasOptionsPage", "Tagging style
")) + self.work_style_box.setWhatsThis(_translate("ClassicalExtrasOptionsPage", "\n" +"\n" +""Tagging style". This section determines how the hierarchy of works will be sourced.
\n" +"Works source: There are 3 options for determing the principal source of the works metadata
\n" +""Use only metadata from title text". The plugin will attempt to extract the hierarchy of works from the track title by looking for repetitions and patterns. If the title does not contain all the work names in the hierarchy then obviously this will limit what can be provided.
\n" +""Use only metadata from canonical works". The hierarchy in the MB database will be used. Assuming the work is correctly entered in MB, this should provide all the data. However the text may differ from the track titles and will be the same for all recordings. It may also be in the language of the composer whereas the titles will probably be in the language of the release. (This language issue can also be addressed by using aliases - see below).
\n" +""Use canonical work metadata enhanced with title text". This supplements the canonical data with text from the titles where it is significantly different. The supplementary title data will be in curly brackets. This is clearly the most complete metadata style of the three but may lead to long descriptions. It is particularly useful for providing translations.
\n" +"Source of canonical work text. Where either of the second two options above are chosen, there is a further choice to be made:
\n" +""Full MusicBrainz work hierarchy". The names of each level of work are used to populate the relevant tags. E.g. if "Má vlast: I. Vyšehrad, JB 1:112/1" (level 0) is part of "Má vlast, JB 1:112" (level 1) then the parent work will be tagged as "Má vlast, JB 1:112", not "Má vlast". So, while accurate, this option might be more verbose.
\n" +""Consistent with lowest level work description". The names of the level 0 work are used to populate the relevant tags. I.e. if "Má vlast: I. Vyšehrad, JB 1:112/1" (level 0) is part of "Má vlast, JB 1:112" (level 1) then the parent work will be tagged as "Má vlast", not "Má vlast, JB 1:112". This frequently looks better, but not always, particularly if the level 0 work name does not contain all the parent work detail. If the full structure is not implicit in the level 0 name then a warning will be logged and written to the "warning" tag.
\n" +"Strategy for setting style: It is suggested that you start with "extended/enhanced" style and the "Consistent with lowest level work description" as the source (this is the default). If this does not give acceptable results, try switching to "Full MusicBrainz work hierarchy". If the "enhanced" details in curly brackets (from the track title) give odd results then switch the style to "canonical works" only. Any remaining oddities are then probably in the MusicBrainz data, which may require editing.
")) + self.works_source_box.setWhatsThis(_translate("ClassicalExtrasOptionsPage", "There are 3 options for determing the principal source of the works metadata
- "Use only metadata from title text". The plugin will attempt to extract the hierarchy of works from the track title by looking for repetitions and patterns. If the title does not contain all the work names in the hierarchy then obviously this will limit what can be provided.
- "Use only metadata from canonical works". The hierarchy in the MB database will be used. Assuming the work is correctly entered in MB, this should provide all the data. However the text may differ from the track titles and will be the same for all recordings. It may also be in the language of the composer whereas the titles will be in the language of the release.
- "Use canonical work metadata enhanced with title text". This supplements the canonical data with text from the titles where it is significantly different. The supplementary data will be in curly brackets. This is clearly the most complete metadata style of the three but may lead to long descriptions. It is particularly useful for providing translations - see image below for an example (using the Muso library manager).
")) + self.works_source_box.setTitle(_translate("ClassicalExtrasOptionsPage", "Works source")) + self.cwp_titles.setWhatsThis(_translate("ClassicalExtrasOptionsPage", ""Use only metadata from title text". The plugin will attempt to extract the hierarchy of works from the track title by looking for repetitions and patterns. If the title does not contain all the work names in the hierarchy then obviously this will limit what can be provided.
")) + self.cwp_titles.setText(_translate("ClassicalExtrasOptionsPage", "Use only metadata from title text")) + self.cwp_works.setWhatsThis(_translate("ClassicalExtrasOptionsPage", ""Use only metadata from canonical works". The hierarchy in the MB database will be used. Assuming the work is correctly entered in MB, this should provide all the data. However the text may differ from the track titles and will be the same for all recordings. It may also be in the language of the composer whereas the titles will probably be in the language of the release. (This language issue can also be addressed by using aliases - see below).
")) + self.cwp_works.setText(_translate("ClassicalExtrasOptionsPage", "Use only metadata from canonical works")) + self.cwp_extended.setWhatsThis(_translate("ClassicalExtrasOptionsPage", ""Use canonical work metadata enhanced with title text". This supplements the canonical data with text from the titles **where it is significantly different**. The supplementary title data will be in curly brackets. This is clearly the most complete metadata style of the three but may lead to long descriptions. It is particularly useful for providing translations
")) + self.cwp_extended.setText(_translate("ClassicalExtrasOptionsPage", "Use canonical work metadata enhanced with title text")) + self.source_of_canonical_box.setWhatsThis(_translate("ClassicalExtrasOptionsPage", "Where either of the second two options above are chosen, there is a further choice to be made:
- "Full MusicBrainz work hierarchy". The names of each level of work are used to populate the relevant tags. I.e. if "Má vlast: I. Vyšehrad, JB 1:112/1" (level 0) is part of "Má vlast, JB 1:112" (level 1) then the parent work will be tagged as "Má vlast, JB 1:112", not "Má vlast". So, while accurate, this option might be more verbose.
- "Consistent with lowest level work description". The names of the level 0 work are used to populate the relevant tags. I.e. if "Má vlast: I. Vyšehrad, JB 1:112/1" (level 0) is part of "Má vlast, JB 1:112" (level 1) then the parent work will be tagged as "Má vlast", not "Má vlast, JB 1:112". This frequently looks better, but not always, particularly if the level 0 work name does not contain all the parent work detail. If the full structure is not implicit in the level 0 name then a warning will be logged and written to the "warning" tag.
")) + self.source_of_canonical_box.setTitle(_translate("ClassicalExtrasOptionsPage", "Source of canonical work text (if applicable)")) + self.cwp_hierarchical_works.setWhatsThis(_translate("ClassicalExtrasOptionsPage", ""Full MusicBrainz work hierarchy". The names of each level of work are used to populate the relevant tags. E.g. if "Má vlast: I. Vyšehrad, JB 1:112/1" (level 0) is part of "Má vlast, JB 1:112" (level 1) then the parent work will be tagged as "Má vlast, JB 1:112", not "Má vlast". So, while accurate, this option might be more verbose.
")) + self.cwp_hierarchical_works.setText(_translate("ClassicalExtrasOptionsPage", "Full MusicBrainz work hierarchy (may be more verbose)")) + self.cwp_level0_works.setWhatsThis(_translate("ClassicalExtrasOptionsPage", ""Consistent with lowest level work description". The names of the level 0 work are used to populate the relevant tags. I.e. if "Má vlast: I. Vyšehrad, JB 1:112/1" (level 0) is part of "Má vlast, JB 1:112" (level 1) then the parent work will be tagged as "Má vlast", not "Má vlast, JB 1:112". This frequently looks better, but not always, particularly if the level 0 work name does not contain all the parent work detail. If the full structure is not implicit in the level 0 name then a warning will be logged and written to the "warning" tag.
")) + self.cwp_level0_works.setText(_translate("ClassicalExtrasOptionsPage", "Consistent with lowest level work description (may be less verbose, but not always complete)")) + self.cwp_derive_works_from_title.setText(_translate("ClassicalExtrasOptionsPage", "Attempt to get works and movement info from title if there are no work relationships? (Requires title in form \"work: movement\")")) + self.work_aliases_label.setText(_translate("ClassicalExtrasOptionsPage", "Aliases (NB Use a consistent approach throughout the library otherwise duplicate works may occur - see the Readme)*
")) + self.work_aliases_box.setWhatsThis(_translate("ClassicalExtrasOptionsPage", ""Replace work names by aliases" will use primary aliases for the chosen locale instead of standard MusicBrainz work names. To choose the locale, use the drop-down under "translate artist names" in the main Picard Options-->Metadata page. Note that this option is not saved as a file tag since, if different choices are made for different releases, different work names may be stored and therefore cannot be grouped together in your player/library manager.
The sub-options allow either the replacement of all work names, where a primary alias exists, just the replacement of work names which are in non-Latin script, or only replace those which are flagged with user "Folksonomy" tags. The tag text needs to be included in the text box, in which case flagged works will be \'aliased\' as well as non-Latin script works, if the second sub-option is chosen. Note that the tags may either be anyone\'s tags ("Look in all tags") or the user\'s own tags. If selecting "Look in user\'s own tags only" you must be logged in to your MusicBrainz user account (in the Picard Options->General page), otherwise repeated dialogue boxes may be generated and you may need to force restart Picard.
")) + self.replace_MBworknames_box.setTitle(_translate("ClassicalExtrasOptionsPage", "Replace MB work names?")) + self.cwp_aliases.setToolTip(_translate("ClassicalExtrasOptionsPage", "Use primary aliases for the chosen locale instead of standard MusicBrainz work names.
")) + self.cwp_aliases.setText(_translate("ClassicalExtrasOptionsPage", "Replace work names by aliases. Select method -->")) + self.cwp_no_aliases.setText(_translate("ClassicalExtrasOptionsPage", "Do not replace work names")) + self.what_to_replace_outer_box.setTitle(_translate("ClassicalExtrasOptionsPage", "What to replace?")) + self.cwp_aliases_all.setText(_translate("ClassicalExtrasOptionsPage", "All work names")) + self.cwp_aliases_greek.setText(_translate("ClassicalExtrasOptionsPage", "Non-latin work names")) + self.cwp_aliases_tagged.setText(_translate("ClassicalExtrasOptionsPage", "Only tagged works")) + self.works_alias_tags_box.setTitle(_translate("ClassicalExtrasOptionsPage", "Tags (\"Folksonomy\") identifying works to be replaced by aliases")) + self.cwp_aliases_tags_all.setText(_translate("ClassicalExtrasOptionsPage", "Look in all tags")) + self.cwp_aliases_tags_user.setText(_translate("ClassicalExtrasOptionsPage", "Look in user\'s own tags only (MUST BE LOGGED IN!)")) + self.cwp_aliases_tag_text.setToolTip(_translate("ClassicalExtrasOptionsPage", "Separate multiple tags by commas
")) + self.work_parts_tags_label.setText(_translate("ClassicalExtrasOptionsPage", "Tags to create - Use commas to separate multiple tags or leave blank to omit
")) + self.works_parts_tags_box.setToolTip(_translate("ClassicalExtrasOptionsPage", "Separate multiple tags by commas.
")) + self.works_parts_tags_box.setWhatsThis(_translate("ClassicalExtrasOptionsPage", ""Tags to create" sets the names of the tags that will be created from the sources described above. All these tags will be blanked before filling as specified. Tags specified against more than one source will have later sources appended in the sequence specified, separated by separators as specified.
")) + self.works_tags_box.setTitle(_translate("ClassicalExtrasOptionsPage", "Work tags")) + self.label_40.setText(_translate("ClassicalExtrasOptionsPage", "Separator")) + self.label_11.setWhatsThis(_translate("ClassicalExtrasOptionsPage", "Some software (notably Muso) can display a 2-level work hierarchy as well as the work-movement hierarchy. This tag can be use to store the 2-level work name (a double colon :: is used to separate the levels within the tag).
")) + self.label_11.setText(_translate("ClassicalExtrasOptionsPage", "Tags for Work - for software with 2-level capability (e.g. Muso)
")) + self.cwp_multi_work_sep.setItemText(1, _translate("ClassicalExtrasOptionsPage", "; ")) + self.cwp_multi_work_sep.setItemText(2, _translate("ClassicalExtrasOptionsPage", ": ")) + self.cwp_multi_work_sep.setItemText(3, _translate("ClassicalExtrasOptionsPage", ". ")) + self.cwp_multi_work_sep.setItemText(4, _translate("ClassicalExtrasOptionsPage", ", ")) + self.cwp_multi_work_sep.setItemText(5, _translate("ClassicalExtrasOptionsPage", "- ")) + self.label.setText(_translate("ClassicalExtrasOptionsPage", "(In this format, intermediate works will be displayed after a double colon :: )")) + self.label_15.setWhatsThis(_translate("ClassicalExtrasOptionsPage", "Software which can display a movement and work (but no higher levels) could use any tags specified here. Note that if there are multiple work levels, the intermediate levels will not be tagged. Users wanting all the information should use the tags from the previous option (but it may cause some breaks in the display if levels change) - alternatively the missing work levels can be included in a movement tag (see below).
")) + self.label_15.setText(_translate("ClassicalExtrasOptionsPage", "Tags for Work - for software with 1-level capability (e.g. iTunes)
")) + self.cwp_single_work_sep.setItemText(1, _translate("ClassicalExtrasOptionsPage", "; ")) + self.cwp_single_work_sep.setItemText(2, _translate("ClassicalExtrasOptionsPage", ": ")) + self.cwp_single_work_sep.setItemText(3, _translate("ClassicalExtrasOptionsPage", ". ")) + self.cwp_single_work_sep.setItemText(4, _translate("ClassicalExtrasOptionsPage", ", ")) + self.cwp_single_work_sep.setItemText(5, _translate("ClassicalExtrasOptionsPage", "- ")) + self.label_2.setText(_translate("ClassicalExtrasOptionsPage", "(Intermediate works will not be displayed:- Either 1. use the 2-level format if you wish to display them, but note that this will ceate new work, or 2. include them in the movement [see below])")) + self.label_12.setWhatsThis(_translate("ClassicalExtrasOptionsPage", "This is the top-level work held in MB. This can be useful for cataloguing and searching (if the library software is capable). Note that this will always be the "canonical" MB name, not one derived from titles or the lowest level work name and that no annotations (e.g. key or work year) will be added. However, if "replace work names by aliases" has been selected and is applicable, the relevant alias will be used.
")) + self.label_12.setText(_translate("ClassicalExtrasOptionsPage", "Tags for top-level (canonical) work (for capable library managers)
")) + self.label_10.setText(_translate("ClassicalExtrasOptionsPage", " N/A ")) + self.parts_tags_box.setTitle(_translate("ClassicalExtrasOptionsPage", "Movement/Part tags")) + self.label_13.setToolTip(_translate("ClassicalExtrasOptionsPage", "The Picard standard tag is \'movementnumber\' - include that or other(s) of your choice
")) + self.label_13.setWhatsThis(_translate("ClassicalExtrasOptionsPage", "This is not necessarily the embedded movt/part number, but is the sequence number of the movement within its parent work on the current release.
")) + self.label_13.setText(_translate("ClassicalExtrasOptionsPage", "Tags for (computed) movement number (Picard std tag is movementnumber)
")) + self.cwp_movt_no_sep.setItemText(1, _translate("ClassicalExtrasOptionsPage", "; ")) + self.cwp_movt_no_sep.setItemText(2, _translate("ClassicalExtrasOptionsPage", ": ")) + self.cwp_movt_no_sep.setItemText(3, _translate("ClassicalExtrasOptionsPage", ". ")) + self.cwp_movt_no_sep.setItemText(4, _translate("ClassicalExtrasOptionsPage", ", ")) + self.cwp_movt_no_sep.setItemText(5, _translate("ClassicalExtrasOptionsPage", "- ")) + self.label_43.setToolTip(_translate("ClassicalExtrasOptionsPage", "The Picard tag \'movementtotal\' will be populated in any case - no need to specify it
")) + self.label_43.setWhatsThis(_translate("ClassicalExtrasOptionsPage", "This is not necessarily the total number of movements in the parent work, but is the total number of movement tracks within the parent work on the current release.
")) + self.label_43.setText(_translate("ClassicalExtrasOptionsPage", "Tags for (computed) total number of movements (Picard std tag is movementtotal)
")) + self.label_79.setText(_translate("ClassicalExtrasOptionsPage", " N/A ")) + self.label_49.setText(_translate("ClassicalExtrasOptionsPage", "Movement name tags (Picard std tag is movement)
Use different movement tags if required for different level systems ==>
for use with multi-level work tags
for use with1-level work tags (intermediate works will prefix movement)
The Picard standard tag is \'movement\' - include that or other(s) of your choice
")) + self.label_14.setWhatsThis(_translate("ClassicalExtrasOptionsPage", "As below, but without the movement part/number prefix (if applicable)
")) + self.label_14.setText(_translate("ClassicalExtrasOptionsPage", "Tags for Movement - excluding embedded movt/part numbers
")) + self.label_39.setText(_translate("ClassicalExtrasOptionsPage", " N/A ")) + self.label_9.setToolTip(_translate("ClassicalExtrasOptionsPage", "The Picard standard tag is \'movement\' - include that or other(s) of your choice
")) + self.label_9.setWhatsThis(_translate("ClassicalExtrasOptionsPage", "This tag(s) will contain the full lowest-level part name extracted from the lowest-level work name, according to the chosen tagging style.
")) + self.label_9.setText(_translate("ClassicalExtrasOptionsPage", "Tags for Movement - including embedded movt/part numbers
")) + self.label_37.setText(_translate("ClassicalExtrasOptionsPage", " N/A ")) + self.partial_arrangements_medleys_label.setText(_translate("ClassicalExtrasOptionsPage", "Partial recordings, arrangements and medleys
")) + self.partial_arrangements_medleys_box.setToolTip(_translate("ClassicalExtrasOptionsPage", "Enter text - do not use any quotation marks
")) + self.label_20.setText(_translate("ClassicalExtrasOptionsPage", "N.B. If these options are selected or deselected, quit and restart Picard before proceeding")) + self.partial_box.setWhatsThis(_translate("ClassicalExtrasOptionsPage", "If this option is selected, partial recordings will be treated as a sub-part of the whole recording and will have the related text included in its name. Note that this text is at the end of the canonical name, but the latter will probably be stripped from the sub-part as it duplicates the recording work name; any title text will be appended to the whole. Note that, if "Consistent with lowest level work description" is chosen in section 2, the text may be treated as a "prefix" similar to those in the "Advanced" tab. If this eliminates other similar prefixes and has unwanted effects, then either change the desired text slightly (e.g. surround with brackets) or use the "Full MusicBrainz work hierarchy" option in section 2.
")) + self.partial_box.setTitle(_translate("ClassicalExtrasOptionsPage", "Partial recordings")) + self.cwp_partial.setText(_translate("ClassicalExtrasOptionsPage", "Show partial recordings as separate sub-part, labelled with ->")) + self.arrangements_box.setWhatsThis(_translate("ClassicalExtrasOptionsPage", "If this option is selected, works which are arrangements of other works will have the latter treated in the same manner as "parent" works, except that the arrangement work name will be prefixed by the text provided.
")) + self.arrangements_box.setTitle(_translate("ClassicalExtrasOptionsPage", "Arrangements")) + self.cwp_arrangements.setText(_translate("ClassicalExtrasOptionsPage", "Show arrangements as parts of original works, labelled with ->")) + self.medleys_box.setTitle(_translate("ClassicalExtrasOptionsPage", "Medleys")) + self.cwp_medley.setWhatsThis(_translate("ClassicalExtrasOptionsPage", "Medleys
These can occur in two ways in MusicBrainz: (a) the recording is described as a "medley of" a number of works and (b) the track is described as (more than one) "medley including a recording of" a work. In the first case, the specified text will be included in brackets after the work name, whereas in the second case, the track will be treated as a recording of multiple works and the specified text will appear in the parent work name.
SongKong-compatible tag usage
")) + self.songkong_box.setToolTip(_translate("ClassicalExtrasOptionsPage", "See "What\'s this"
")) + self.songkong_box.setWhatsThis(_translate("ClassicalExtrasOptionsPage", ""Use work tags on file (no look up on MB) if Use Cache selected": This will enable the existing work tags on the file to be used in preference to looking up on MusicBrainz, if those tags are SongKong-compatible (which should be the case if SongKong has been used or if the SongKong tags have been previously written by this plugin). If present, this can speed up processing considerably, but obviously any new data on MusicBrainz will be missed. For the option to operate, "Use cache" also needs to be selected. Although faster, some of the subtleties of a full look-up will be missed - for example, parent works which are arrangements will not be highlighted as such, some arrangers or composers of original works may be omitted and some medley information may be missed. **In general, therefore, the use of this option will result in poorer metadata than allowing the full database look-up to run. It is not recommended unless speed is more important than quality.**
"Write SongKong-compatible work tags" does what it says. These can then be used by the previous option, if the release is subsequently reloaded into Picard, to speed things up (assuming the reload was not to pick up new work data).
Note that Picard and SongKong use the tag musicbrainz_workid to mean different things. If Picard has overwritten the SongKong tag (not a problem if this plugin is used) then a warning will be given and the works will be looked up on MusicBrainz. Also note that once a release is loaded, subsequent refreshes will use the cache (if option is ticked) in preference to the file tags.
")) + self.cwp_use_sk.setText(_translate("ClassicalExtrasOptionsPage", "Use work tags on file (no look up on MB) if Use Cache selected* (NOT RECOMMENDED - SEE README)")) + self.cwp_write_sk.setText(_translate("ClassicalExtrasOptionsPage", "Write SongKong-compatible work tags*")) + self.label_82.setText(_translate("ClassicalExtrasOptionsPage", "* ASTERISKED OPTIONS ARE NOT SAVED IN FILE TAGS")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.Works), _translate("ClassicalExtrasOptionsPage", "Works and parts")) + self.genre_tag_label.setText(_translate("ClassicalExtrasOptionsPage", "Genre tags
")) + self.label_73.setText(_translate("ClassicalExtrasOptionsPage", "Name of genre tag")) + self.label_74.setText(_translate("ClassicalExtrasOptionsPage", "Name of sub-genre tag")) + self.classical_genre_label.setText(_translate("ClassicalExtrasOptionsPage", ""Classical" genre
")) + self.cwp_genres_classical_exclude.setText(_translate("ClassicalExtrasOptionsPage", "Exclude the text \"classical\" from main genre tag even if listed above")) + self.cwp_genres_classical_selective.setText(_translate("ClassicalExtrasOptionsPage", "Make track \"classical\" only if there is a classical-specific genre (or do nothing if there is no filter)")) + self.cwp_muso_classical.setText(_translate("ClassicalExtrasOptionsPage", "Use Muso composer list to determine if classical*")) + self.cwp_genres_classical_all.setText(_translate("ClassicalExtrasOptionsPage", "Make all tracks \"classical\"")) + self.label_64.setText(_translate("ClassicalExtrasOptionsPage", "Write a flag with text =")) + self.label_71.setText(_translate("ClassicalExtrasOptionsPage", " in the following tag if the track is classical")) + self.cwp_genres_arranger_as_composer.setText(_translate("ClassicalExtrasOptionsPage", "(Treat arrangers as for composers)")) + self.label_81.setText(_translate("ClassicalExtrasOptionsPage", "* ASTERISKED OPTIONS ARE NOT SAVED IN FILE TAGS")) + self.cwp_use_muso_refdb.setText(_translate("ClassicalExtrasOptionsPage", "Use Muso reference database (default path is set on \"advanced\" tab)*")) + self.instruments_keys_label.setText(_translate("ClassicalExtrasOptionsPage", "Instruments and keys
")) + self.instruments_box.setTitle(_translate("ClassicalExtrasOptionsPage", "Instruments")) + self.label_66.setText(_translate("ClassicalExtrasOptionsPage", "Tag name for instruments (will hold all instruments for a track)")) + self.instruments_source_box.setTitle(_translate("ClassicalExtrasOptionsPage", "Name sources to use for instruments (select at least one, otherwise no instruments will be included in tag)")) + self.cwp_instruments_MB_names.setText(_translate("ClassicalExtrasOptionsPage", "MusicBrainz standard names")) + self.cwp_instruments_credited_names.setText(_translate("ClassicalExtrasOptionsPage", "\"Credited-as\" names")) + self.keys_box.setTitle(_translate("ClassicalExtrasOptionsPage", "Keys")) + self.label_72.setText(_translate("ClassicalExtrasOptionsPage", "Tag name for key(s)")) + self.keys_include_box.setTitle(_translate("ClassicalExtrasOptionsPage", "Include key(s) in work name?")) + self.cwp_key_never_include.setText(_translate("ClassicalExtrasOptionsPage", "Never")) + self.cwp_key_contingent_include.setText(_translate("ClassicalExtrasOptionsPage", "Only if key not already mentioned in work name")) + self.cwp_key_include.setWhatsThis(_translate("ClassicalExtrasOptionsPage", ""Include key(s) in work names" gives the option to include the key signature for a work in brackets after the name of the work in the metadata. Keys will be added in the appropriate levels: e.g. Dvořák\'s New World Symphony will get (E minor) at the work level, but only movements with different keys will be annotated viz. "II. Largo (D-flat major, C-Sharp minor)"
")) + self.cwp_key_include.setText(_translate("ClassicalExtrasOptionsPage", "Always")) + self.allowed_filters_label.setText(_translate("ClassicalExtrasOptionsPage", "Allowed genres (filter)
")) + self.cwp_genres_filter.setText(_translate("ClassicalExtrasOptionsPage", "Only apply genres to tags if they match pre-defined names:")) + self.genre_filters_frame.setWhatsThis(_translate("ClassicalExtrasOptionsPage", "Explanation of genre-matching:
Only genres matching those in the boxes will be placed in the genre or sub-genre tags.
If there is a matching genre found in the "classical main genres" or "classical sub-genres" box, then the track will be treated as being classical.
")) + self.classical_genres_box.setTitle(_translate("ClassicalExtrasOptionsPage", "Classical genres (i.e. specific to classical music) - List separated by commas")) + self.label_60.setText(_translate("ClassicalExtrasOptionsPage", "Main genres:")) + self.label_75.setText(_translate("ClassicalExtrasOptionsPage", "Sub-genres:")) + self.cwp_muso_genres.setToolTip(_translate("ClassicalExtrasOptionsPage", "Select this to use the "classical genres" in Muso options as the "Main classical genres" here.
")) + self.cwp_muso_genres.setText(_translate("ClassicalExtrasOptionsPage", "Use Muso classical genres*")) + self.general_genres_box.setTitle(_translate("ClassicalExtrasOptionsPage", "General genres (may be associated with classical music, but not necessarily, e.g. \"instrumental\") - List separated by commas")) + self.label_62.setText(_translate("ClassicalExtrasOptionsPage", "Main genres")) + self.label_76.setText(_translate("ClassicalExtrasOptionsPage", "Sub-genres")) + self.label_80.setText(_translate("ClassicalExtrasOptionsPage", "Genre name to use if none of the above main genres apply (leave blank if not required)")) + self.label_77.setText(_translate("ClassicalExtrasOptionsPage", "List genres, separated by commas. Only those genres listed will be included in tags.")) + self.label_78.setText(_translate("ClassicalExtrasOptionsPage", "See \"what\'s this\" for more details.")) + self.source_of_genres_label.setText(_translate("ClassicalExtrasOptionsPage", "Source of genres - Note: if "existing file tag" is selected, information from the tag "genre" and the genre tag name specified above (if different) will be used
")) + self.cwp_genres_use_file.setToolTip(_translate("ClassicalExtrasOptionsPage", "NB: This will use the contents of the file tag with the name given above (usually \'genre\').
")) + self.cwp_genres_use_file.setText(_translate("ClassicalExtrasOptionsPage", "Existing file tag (see note above)")) + self.cwp_genres_use_folks.setWhatsThis(_translate("ClassicalExtrasOptionsPage", "This will use the folksonomy tags for works as a possible source of genres (if they match one of the lists below).
To use the folksonomy tags for releases/tracks, select the main Picard option in Options->Metadata->"Use folksonomy tags as genre". Again (unlike vanilla Picard) they will only be used by this plugin if they match one of the lists below.
")) + self.cwp_genres_use_folks.setText(_translate("ClassicalExtrasOptionsPage", "Folksonomy work tags")) + self.cwp_genres_use_worktype.setText(_translate("ClassicalExtrasOptionsPage", "Work-type")) + self.cwp_genres_infer.setText(_translate("ClassicalExtrasOptionsPage", "Infer from artist metadata")) + self.label_109.setText(_translate("ClassicalExtrasOptionsPage", "Periods and dates
")) + self.dates_box.setTitle(_translate("ClassicalExtrasOptionsPage", "Work dates")) + self.dates_box_inner.setTitle(_translate("ClassicalExtrasOptionsPage", "Source of work dates for above tag")) + self.cwp_workdate_source_composed.setText(_translate("ClassicalExtrasOptionsPage", "Composed date (or parent composed date)")) + self.cwp_workdate_source_published.setText(_translate("ClassicalExtrasOptionsPage", "Published date")) + self.cwp_workdate_source_premiered.setText(_translate("ClassicalExtrasOptionsPage", "Premiered date")) + self.cwp_workdate_use_first.setText(_translate("ClassicalExtrasOptionsPage", "Use first available of above (in listed order)")) + self.cwp_workdate_use_all.setText(_translate("ClassicalExtrasOptionsPage", "Include all sources")) + self.cwp_workdate_annotate.setText(_translate("ClassicalExtrasOptionsPage", "Annotate dates using source name")) + self.label_68.setText(_translate("ClassicalExtrasOptionsPage", "Tag name for work date")) + self.cwp_workdate_include.setWhatsThis(_translate("ClassicalExtrasOptionsPage", ""Includeworkdate in work names" gives the option to include the \'work year\' for a work in brackets after the name of the work in the metadata. Dates (years) will be added in the appropriate levels: e.g. Smetana\'s \'Má vlast\' will get (1874-1879) at the work level, but the movements with different dates will be annotated viz. "Vyšehrad, JB 1:112/1 (1874)". If the dates are the same, there should be no repitetion at the movement level. (Work dates will be used in preference order, i.e. composed - published - premiered, with only the first available date being shown).
")) + self.cwp_workdate_include.setText(_translate("ClassicalExtrasOptionsPage", "Include workdate in work name (in preference order listed above, with no annotation)")) + self.periods_box.setTitle(_translate("ClassicalExtrasOptionsPage", "Periods")) + self.label_69.setText(_translate("ClassicalExtrasOptionsPage", "Tag name for period")) + self.cwp_muso_periods.setText(_translate("ClassicalExtrasOptionsPage", "Use Muso map*")) + self.cwp_muso_dates.setText(_translate("ClassicalExtrasOptionsPage", "Use Muso composer dates (if no work date) to determine period*")) + self.label_70.setText(_translate("ClassicalExtrasOptionsPage", "Period map:")) + self.period_map_annotation_label.setText(_translate("ClassicalExtrasOptionsPage", " (Period name, Start year, End year; Period name2, ... etc.) - periods may overlap [Do not use commas or semi-colons within period name]")) + self.cwp_periods_arranger_as_composer.setText(_translate("ClassicalExtrasOptionsPage", "(Treat arrangers as for composers)")) + self.label_119.setText(_translate("ClassicalExtrasOptionsPage", "N.B. At least one of the first two tabs (Artists: "Create extra artist metadata", or Works and parts: "Include all work levels") must be enabled for this section to run.
(Functionality will be reduced unless both the first two tabs are enabled.)
N.B. At least one of the first two tabs (Artists: "Create extra artist metadata", or Works and parts: "Include all work levels") must be enabled for this section to run.
")) + self.label_97.setText(_translate("ClassicalExtrasOptionsPage", "Initial tag processing
")) + self.initial_tag_processing_box.setWhatsThis(_translate("ClassicalExtrasOptionsPage", "Any tags specified in the next two rows will be blanked before applying the tag sources described in the following section. NB this applies only to Picard-generated tags, not to other tags which might pre-exist on the file: to blank those, use the main Options->Tags page. Comma-separate the tag names within the rows and note that these names are case-sensitive.
")) + self.tags_to_blank.setTitle(_translate("ClassicalExtrasOptionsPage", "Remove Picard-generated tags before applying subsequent actions? (NB existing LOCAL FILE tags will remain unless cleared using standard Picard options - to remove these, overwrite them in the next section)")) + self.label_3.setText(_translate("ClassicalExtrasOptionsPage", "Picard-generated tags to blank (comma-separated, case-sensitive):
")) + self.cea_blank_tag.setToolTip(_translate("ClassicalExtrasOptionsPage", "Enter file tag names, separated by commas
")) + self.cea_blank_tag_2.setToolTip(_translate("ClassicalExtrasOptionsPage", "Enter file tag names, separated by commas
")) + self.label_19.setWhatsThis(_translate("ClassicalExtrasOptionsPage", "List existing file tags which will be appended to rather than over-written by tag mapping (this will keep tags even if "Clear existing tags" is selected on main options)
NB To allow appending to happen, do not also include these tags in "Preserve tags" on the main options.
Enter file tag names, separated by commas
")) + self.cea_keep.setWhatsThis(_translate("ClassicalExtrasOptionsPage", "This refers to the tags which already exist on files which have been matched to MusicBrainz in the right-hand panel, not the tags generated by Picard from the MusicBrainz database. Normally, Picard cannot process these tags - either it will overwrite them (if it creates a similarly named tag), clear them (if \'Clear existing tags\' is specified in the main Options->Tags screen) or keep them (if \'Preserve these tags...\' is specified after the \'Clear existing tags\' option). Classical Extras allows a further option - for the tags to be appended to in the tag mapping section (see below). List file tags which will be appended to rather than over-written by tag mapping (NB this will keep tags even if "Clear existing tags" is selected on main options). In addition, certain existing tags may be used by Classical Extras - in particular genre-related tags (see the Genres etc. options tab for more).
Note that if "Split lyrics tag" is specified (see the Artists tab), then the tag named there will be included in the \'Keep file tags\' list and does not need to be added in this section.
")) + self.cea_clear_tags.setToolTip(_translate("ClassicalExtrasOptionsPage", "Note that the main Picard option "Clear existing tags" should be unchecked for this option to operate in preference to that Picard option. The difference is that this option will not intefere with cover art, whereas the main Picard option will remove previous cover art.
")) + self.cea_clear_tags.setWhatsThis(_translate("ClassicalExtrasOptionsPage", "If selected: the bottom pane of Picard will only show tags which have been generated from the MusicBrainz lookups plus any existing file tags which are listed above or in the main options "Preserve tags...".
This does not mean that the file tags will be removed when saving the file. For that to happen, "Clear existing tags" needs to be selected in the main options.
")) + self.cea_clear_tags.setText(_translate("ClassicalExtrasOptionsPage", "Do not show any file tags that are NOT listed above AND NOT listed in the main Picard \"Preserve tags...\" option (Options->Tags), even if \"Clear existing tags\" is not selected.")) + self.label_98.setText(_translate("ClassicalExtrasOptionsPage", "Tag map details
")) + self.tagmap_details_box.setToolTip(_translate("ClassicalExtrasOptionsPage", "Enter tags, separated by commas.
")) + self.textBrowser.setHtml(_translate("ClassicalExtrasOptionsPage", "\n" +"\n" +"Notes:
\n" +"Click "Source from:" button to edit source tags.
\n" +"Any valid Picard-generated tag can be entered in the "source" box, as well as Classical Extras sources, and mapped into other tags - not just restricted to artists.
\n" +"To put a constant in a tag, type it into the source box preceded by a backslash \\.
\n" +"In all cases, the source will be APPENDED to the Picard tag. To replace the standard tag, first blank it in the section above - add it back later in the list below if required (e.g. artist -> artist).
\n" +"BUT note that any existing LOCAL FILE tag will be replaced by (not appended with) any Picard/Classical Extras tag UNLESS specified in the list box above.
\n" +"These tag-mapping options may be omitted from the over-riding of artist options - see advanced tab
\n" +"For more help seethe readme.
")) + self.toolButton_1.setToolTip(_translate("ClassicalExtrasOptionsPage", "Click to edit sources")) + self.toolButton_1.setText(_translate("ClassicalExtrasOptionsPage", "Source from:")) + self.cea_source_1.setToolTip(_translate("ClassicalExtrasOptionsPage", "Click button to edit. See notes above.")) + self.cea_source_1.setItemText(1, _translate("ClassicalExtrasOptionsPage", "album_soloists, album_conductors, album_ensembles")) + self.cea_source_1.setItemText(2, _translate("ClassicalExtrasOptionsPage", "soloists, conductors, ensembles, album_composers, composers")) + self.cea_source_1.setItemText(3, _translate("ClassicalExtrasOptionsPage", "album_soloists")) + self.cea_source_1.setItemText(4, _translate("ClassicalExtrasOptionsPage", "album_conductors")) + self.cea_source_1.setItemText(5, _translate("ClassicalExtrasOptionsPage", "album_ensembles")) + self.cea_source_1.setItemText(6, _translate("ClassicalExtrasOptionsPage", "album_composers")) + self.cea_source_1.setItemText(7, _translate("ClassicalExtrasOptionsPage", "album_composer_lastnames")) + self.cea_source_1.setItemText(8, _translate("ClassicalExtrasOptionsPage", "soloists")) + self.cea_source_1.setItemText(9, _translate("ClassicalExtrasOptionsPage", "soloist_names")) + self.cea_source_1.setItemText(10, _translate("ClassicalExtrasOptionsPage", "ensembles")) + self.cea_source_1.setItemText(11, _translate("ClassicalExtrasOptionsPage", "ensemble_names")) + self.cea_source_1.setItemText(12, _translate("ClassicalExtrasOptionsPage", "composers")) + self.cea_source_1.setItemText(13, _translate("ClassicalExtrasOptionsPage", "arrangers")) + self.cea_source_1.setItemText(14, _translate("ClassicalExtrasOptionsPage", "orchestrators")) + self.cea_source_1.setItemText(15, _translate("ClassicalExtrasOptionsPage", "conductors")) + self.cea_source_1.setItemText(16, _translate("ClassicalExtrasOptionsPage", "chorusmasters")) + self.cea_source_1.setItemText(17, _translate("ClassicalExtrasOptionsPage", "leaders")) + self.cea_source_1.setItemText(18, _translate("ClassicalExtrasOptionsPage", "support_performers")) + self.cea_source_1.setItemText(19, _translate("ClassicalExtrasOptionsPage", "work_type")) + self.cea_source_1.setItemText(20, _translate("ClassicalExtrasOptionsPage", "release")) + self.label_21.setText(_translate("ClassicalExtrasOptionsPage", "into tags:")) + self.cea_tag_1.setToolTip(_translate("ClassicalExtrasOptionsPage", "Enter comma-separated list of tags")) + self.cea_cond_1.setText(_translate("ClassicalExtrasOptionsPage", "Conditional?")) + self.toolButton_2.setToolTip(_translate("ClassicalExtrasOptionsPage", "Click to edit sources")) + self.toolButton_2.setText(_translate("ClassicalExtrasOptionsPage", "Source from:")) + self.cea_source_2.setToolTip(_translate("ClassicalExtrasOptionsPage", "Click button to edit. See notes above.")) + self.cea_source_2.setItemText(1, _translate("ClassicalExtrasOptionsPage", "album_soloists, album_conductors, album_ensembles")) + self.cea_source_2.setItemText(2, _translate("ClassicalExtrasOptionsPage", "soloists, conductors, ensembles, album_composers, composers")) + self.cea_source_2.setItemText(3, _translate("ClassicalExtrasOptionsPage", "album_soloists")) + self.cea_source_2.setItemText(4, _translate("ClassicalExtrasOptionsPage", "album_conductors")) + self.cea_source_2.setItemText(5, _translate("ClassicalExtrasOptionsPage", "album_ensembles")) + self.cea_source_2.setItemText(6, _translate("ClassicalExtrasOptionsPage", "album_composers")) + self.cea_source_2.setItemText(7, _translate("ClassicalExtrasOptionsPage", "album_composer_lastnames")) + self.cea_source_2.setItemText(8, _translate("ClassicalExtrasOptionsPage", "soloists")) + self.cea_source_2.setItemText(9, _translate("ClassicalExtrasOptionsPage", "soloist_names")) + self.cea_source_2.setItemText(10, _translate("ClassicalExtrasOptionsPage", "ensembles")) + self.cea_source_2.setItemText(11, _translate("ClassicalExtrasOptionsPage", "ensemble_names")) + self.cea_source_2.setItemText(12, _translate("ClassicalExtrasOptionsPage", "composers")) + self.cea_source_2.setItemText(13, _translate("ClassicalExtrasOptionsPage", "arrangers")) + self.cea_source_2.setItemText(14, _translate("ClassicalExtrasOptionsPage", "orchestrators")) + self.cea_source_2.setItemText(15, _translate("ClassicalExtrasOptionsPage", "conductors")) + self.cea_source_2.setItemText(16, _translate("ClassicalExtrasOptionsPage", "chorusmasters")) + self.cea_source_2.setItemText(17, _translate("ClassicalExtrasOptionsPage", "leaders")) + self.cea_source_2.setItemText(18, _translate("ClassicalExtrasOptionsPage", "support_performers")) + self.cea_source_2.setItemText(19, _translate("ClassicalExtrasOptionsPage", "work_type")) + self.cea_source_2.setItemText(20, _translate("ClassicalExtrasOptionsPage", "release")) + self.label_23.setText(_translate("ClassicalExtrasOptionsPage", "into tags:")) + self.cea_tag_2.setToolTip(_translate("ClassicalExtrasOptionsPage", "Enter comma-separated list of tags")) + self.cea_cond_2.setText(_translate("ClassicalExtrasOptionsPage", "Conditional?")) + self.toolButton_3.setToolTip(_translate("ClassicalExtrasOptionsPage", "Click to edit sources")) + self.toolButton_3.setText(_translate("ClassicalExtrasOptionsPage", "Source from:")) + self.cea_source_3.setToolTip(_translate("ClassicalExtrasOptionsPage", "Click button to edit. See notes above.")) + self.cea_source_3.setItemText(1, _translate("ClassicalExtrasOptionsPage", "album_soloists, album_conductors, album_ensembles")) + self.cea_source_3.setItemText(2, _translate("ClassicalExtrasOptionsPage", "soloists, conductors, ensembles, album_composers, composers")) + self.cea_source_3.setItemText(3, _translate("ClassicalExtrasOptionsPage", "album_soloists")) + self.cea_source_3.setItemText(4, _translate("ClassicalExtrasOptionsPage", "album_conductors")) + self.cea_source_3.setItemText(5, _translate("ClassicalExtrasOptionsPage", "album_ensembles")) + self.cea_source_3.setItemText(6, _translate("ClassicalExtrasOptionsPage", "album_composers")) + self.cea_source_3.setItemText(7, _translate("ClassicalExtrasOptionsPage", "album_composer_lastnames")) + self.cea_source_3.setItemText(8, _translate("ClassicalExtrasOptionsPage", "soloists")) + self.cea_source_3.setItemText(9, _translate("ClassicalExtrasOptionsPage", "soloist_names")) + self.cea_source_3.setItemText(10, _translate("ClassicalExtrasOptionsPage", "ensembles")) + self.cea_source_3.setItemText(11, _translate("ClassicalExtrasOptionsPage", "ensemble_names")) + self.cea_source_3.setItemText(12, _translate("ClassicalExtrasOptionsPage", "composers")) + self.cea_source_3.setItemText(13, _translate("ClassicalExtrasOptionsPage", "arrangers")) + self.cea_source_3.setItemText(14, _translate("ClassicalExtrasOptionsPage", "orchestrators")) + self.cea_source_3.setItemText(15, _translate("ClassicalExtrasOptionsPage", "conductors")) + self.cea_source_3.setItemText(16, _translate("ClassicalExtrasOptionsPage", "chorusmasters")) + self.cea_source_3.setItemText(17, _translate("ClassicalExtrasOptionsPage", "leaders")) + self.cea_source_3.setItemText(18, _translate("ClassicalExtrasOptionsPage", "support_performers")) + self.cea_source_3.setItemText(19, _translate("ClassicalExtrasOptionsPage", "work_type")) + self.cea_source_3.setItemText(20, _translate("ClassicalExtrasOptionsPage", "release")) + self.label_25.setText(_translate("ClassicalExtrasOptionsPage", "into tags:")) + self.cea_tag_3.setToolTip(_translate("ClassicalExtrasOptionsPage", "Enter comma-separated list of tags")) + self.cea_cond_3.setText(_translate("ClassicalExtrasOptionsPage", "Conditional?")) + self.toolButton_4.setToolTip(_translate("ClassicalExtrasOptionsPage", "Click to edit sources")) + self.toolButton_4.setText(_translate("ClassicalExtrasOptionsPage", "Source from:")) + self.cea_source_4.setToolTip(_translate("ClassicalExtrasOptionsPage", "Click button to edit. See notes above.")) + self.cea_source_4.setItemText(1, _translate("ClassicalExtrasOptionsPage", "album_soloists, album_conductors, album_ensembles")) + self.cea_source_4.setItemText(2, _translate("ClassicalExtrasOptionsPage", "soloists, conductors, ensembles, album_composers, composers")) + self.cea_source_4.setItemText(3, _translate("ClassicalExtrasOptionsPage", "album_soloists")) + self.cea_source_4.setItemText(4, _translate("ClassicalExtrasOptionsPage", "album_conductors")) + self.cea_source_4.setItemText(5, _translate("ClassicalExtrasOptionsPage", "album_ensembles")) + self.cea_source_4.setItemText(6, _translate("ClassicalExtrasOptionsPage", "album_composers")) + self.cea_source_4.setItemText(7, _translate("ClassicalExtrasOptionsPage", "album_composer_lastnames")) + self.cea_source_4.setItemText(8, _translate("ClassicalExtrasOptionsPage", "soloists")) + self.cea_source_4.setItemText(9, _translate("ClassicalExtrasOptionsPage", "soloist_names")) + self.cea_source_4.setItemText(10, _translate("ClassicalExtrasOptionsPage", "ensembles")) + self.cea_source_4.setItemText(11, _translate("ClassicalExtrasOptionsPage", "ensemble_names")) + self.cea_source_4.setItemText(12, _translate("ClassicalExtrasOptionsPage", "composers")) + self.cea_source_4.setItemText(13, _translate("ClassicalExtrasOptionsPage", "arrangers")) + self.cea_source_4.setItemText(14, _translate("ClassicalExtrasOptionsPage", "orchestrators")) + self.cea_source_4.setItemText(15, _translate("ClassicalExtrasOptionsPage", "conductors")) + self.cea_source_4.setItemText(16, _translate("ClassicalExtrasOptionsPage", "chorusmasters")) + self.cea_source_4.setItemText(17, _translate("ClassicalExtrasOptionsPage", "leaders")) + self.cea_source_4.setItemText(18, _translate("ClassicalExtrasOptionsPage", "support_performers")) + self.cea_source_4.setItemText(19, _translate("ClassicalExtrasOptionsPage", "work_type")) + self.cea_source_4.setItemText(20, _translate("ClassicalExtrasOptionsPage", "release")) + self.label_27.setText(_translate("ClassicalExtrasOptionsPage", "into tags:")) + self.cea_tag_4.setToolTip(_translate("ClassicalExtrasOptionsPage", "Enter comma-separated list of tags")) + self.cea_cond_4.setText(_translate("ClassicalExtrasOptionsPage", "Conditional?")) + self.toolButton_5.setToolTip(_translate("ClassicalExtrasOptionsPage", "Click to edit sources")) + self.toolButton_5.setText(_translate("ClassicalExtrasOptionsPage", "Source from:")) + self.cea_source_5.setToolTip(_translate("ClassicalExtrasOptionsPage", "Click button to edit. See notes above.")) + self.cea_source_5.setItemText(1, _translate("ClassicalExtrasOptionsPage", "album_soloists, album_conductors, album_ensembles")) + self.cea_source_5.setItemText(2, _translate("ClassicalExtrasOptionsPage", "soloists, conductors, ensembles, album_composers, composers")) + self.cea_source_5.setItemText(3, _translate("ClassicalExtrasOptionsPage", "album_soloists")) + self.cea_source_5.setItemText(4, _translate("ClassicalExtrasOptionsPage", "album_conductors")) + self.cea_source_5.setItemText(5, _translate("ClassicalExtrasOptionsPage", "album_ensembles")) + self.cea_source_5.setItemText(6, _translate("ClassicalExtrasOptionsPage", "album_composers")) + self.cea_source_5.setItemText(7, _translate("ClassicalExtrasOptionsPage", "album_composer_lastnames")) + self.cea_source_5.setItemText(8, _translate("ClassicalExtrasOptionsPage", "soloists")) + self.cea_source_5.setItemText(9, _translate("ClassicalExtrasOptionsPage", "soloist_names")) + self.cea_source_5.setItemText(10, _translate("ClassicalExtrasOptionsPage", "ensembles")) + self.cea_source_5.setItemText(11, _translate("ClassicalExtrasOptionsPage", "ensemble_names")) + self.cea_source_5.setItemText(12, _translate("ClassicalExtrasOptionsPage", "composers")) + self.cea_source_5.setItemText(13, _translate("ClassicalExtrasOptionsPage", "arrangers")) + self.cea_source_5.setItemText(14, _translate("ClassicalExtrasOptionsPage", "orchestrators")) + self.cea_source_5.setItemText(15, _translate("ClassicalExtrasOptionsPage", "conductors")) + self.cea_source_5.setItemText(16, _translate("ClassicalExtrasOptionsPage", "chorusmasters")) + self.cea_source_5.setItemText(17, _translate("ClassicalExtrasOptionsPage", "leaders")) + self.cea_source_5.setItemText(18, _translate("ClassicalExtrasOptionsPage", "support_performers")) + self.cea_source_5.setItemText(19, _translate("ClassicalExtrasOptionsPage", "work_type")) + self.cea_source_5.setItemText(20, _translate("ClassicalExtrasOptionsPage", "release")) + self.label_29.setText(_translate("ClassicalExtrasOptionsPage", "into tags:")) + self.cea_tag_5.setToolTip(_translate("ClassicalExtrasOptionsPage", "Enter comma-separated list of tags")) + self.cea_cond_5.setText(_translate("ClassicalExtrasOptionsPage", "Conditional?")) + self.toolButton_6.setToolTip(_translate("ClassicalExtrasOptionsPage", "Click to edit sources")) + self.toolButton_6.setText(_translate("ClassicalExtrasOptionsPage", "Source from:")) + self.cea_source_6.setToolTip(_translate("ClassicalExtrasOptionsPage", "Click button to edit. See notes above.")) + self.cea_source_6.setItemText(1, _translate("ClassicalExtrasOptionsPage", "album_soloists, album_conductors, album_ensembles")) + self.cea_source_6.setItemText(2, _translate("ClassicalExtrasOptionsPage", "soloists, conductors, ensembles, album_composers, composers")) + self.cea_source_6.setItemText(3, _translate("ClassicalExtrasOptionsPage", "album_soloists")) + self.cea_source_6.setItemText(4, _translate("ClassicalExtrasOptionsPage", "album_conductors")) + self.cea_source_6.setItemText(5, _translate("ClassicalExtrasOptionsPage", "album_ensembles")) + self.cea_source_6.setItemText(6, _translate("ClassicalExtrasOptionsPage", "album_composers")) + self.cea_source_6.setItemText(7, _translate("ClassicalExtrasOptionsPage", "album_composer_lastnames")) + self.cea_source_6.setItemText(8, _translate("ClassicalExtrasOptionsPage", "soloists")) + self.cea_source_6.setItemText(9, _translate("ClassicalExtrasOptionsPage", "soloist_names")) + self.cea_source_6.setItemText(10, _translate("ClassicalExtrasOptionsPage", "ensembles")) + self.cea_source_6.setItemText(11, _translate("ClassicalExtrasOptionsPage", "ensemble_names")) + self.cea_source_6.setItemText(12, _translate("ClassicalExtrasOptionsPage", "composers")) + self.cea_source_6.setItemText(13, _translate("ClassicalExtrasOptionsPage", "arrangers")) + self.cea_source_6.setItemText(14, _translate("ClassicalExtrasOptionsPage", "orchestrators")) + self.cea_source_6.setItemText(15, _translate("ClassicalExtrasOptionsPage", "conductors")) + self.cea_source_6.setItemText(16, _translate("ClassicalExtrasOptionsPage", "chorusmasters")) + self.cea_source_6.setItemText(17, _translate("ClassicalExtrasOptionsPage", "leaders")) + self.cea_source_6.setItemText(18, _translate("ClassicalExtrasOptionsPage", "support_performers")) + self.cea_source_6.setItemText(19, _translate("ClassicalExtrasOptionsPage", "work_type")) + self.cea_source_6.setItemText(20, _translate("ClassicalExtrasOptionsPage", "release")) + self.label_31.setText(_translate("ClassicalExtrasOptionsPage", "into tags:")) + self.cea_tag_6.setToolTip(_translate("ClassicalExtrasOptionsPage", "Enter comma-separated list of tags")) + self.cea_cond_6.setText(_translate("ClassicalExtrasOptionsPage", "Conditional?")) + self.toolButton_7.setToolTip(_translate("ClassicalExtrasOptionsPage", "Click to edit sources")) + self.toolButton_7.setText(_translate("ClassicalExtrasOptionsPage", "Source from:")) + self.cea_source_7.setToolTip(_translate("ClassicalExtrasOptionsPage", "Click button to edit. See notes above.")) + self.cea_source_7.setItemText(1, _translate("ClassicalExtrasOptionsPage", "album_soloists, album_conductors, album_ensembles")) + self.cea_source_7.setItemText(2, _translate("ClassicalExtrasOptionsPage", "soloists, conductors, ensembles, album_composers, composers")) + self.cea_source_7.setItemText(3, _translate("ClassicalExtrasOptionsPage", "album_soloists")) + self.cea_source_7.setItemText(4, _translate("ClassicalExtrasOptionsPage", "album_conductors")) + self.cea_source_7.setItemText(5, _translate("ClassicalExtrasOptionsPage", "album_ensembles")) + self.cea_source_7.setItemText(6, _translate("ClassicalExtrasOptionsPage", "album_composers")) + self.cea_source_7.setItemText(7, _translate("ClassicalExtrasOptionsPage", "album_composer_lastnames")) + self.cea_source_7.setItemText(8, _translate("ClassicalExtrasOptionsPage", "soloists")) + self.cea_source_7.setItemText(9, _translate("ClassicalExtrasOptionsPage", "soloist_names")) + self.cea_source_7.setItemText(10, _translate("ClassicalExtrasOptionsPage", "ensembles")) + self.cea_source_7.setItemText(11, _translate("ClassicalExtrasOptionsPage", "ensemble_names")) + self.cea_source_7.setItemText(12, _translate("ClassicalExtrasOptionsPage", "composers")) + self.cea_source_7.setItemText(13, _translate("ClassicalExtrasOptionsPage", "arrangers")) + self.cea_source_7.setItemText(14, _translate("ClassicalExtrasOptionsPage", "orchestrators")) + self.cea_source_7.setItemText(15, _translate("ClassicalExtrasOptionsPage", "conductors")) + self.cea_source_7.setItemText(16, _translate("ClassicalExtrasOptionsPage", "chorusmasters")) + self.cea_source_7.setItemText(17, _translate("ClassicalExtrasOptionsPage", "leaders")) + self.cea_source_7.setItemText(18, _translate("ClassicalExtrasOptionsPage", "support_performers")) + self.cea_source_7.setItemText(19, _translate("ClassicalExtrasOptionsPage", "work_type")) + self.cea_source_7.setItemText(20, _translate("ClassicalExtrasOptionsPage", "release")) + self.label_33.setText(_translate("ClassicalExtrasOptionsPage", "into tags:")) + self.cea_tag_7.setToolTip(_translate("ClassicalExtrasOptionsPage", "Enter comma-separated list of tags")) + self.cea_cond_7.setText(_translate("ClassicalExtrasOptionsPage", "Conditional?")) + self.toolButton_8.setToolTip(_translate("ClassicalExtrasOptionsPage", "Click to edit sources")) + self.toolButton_8.setText(_translate("ClassicalExtrasOptionsPage", "Source from:")) + self.cea_source_8.setToolTip(_translate("ClassicalExtrasOptionsPage", "Click button to edit. See notes above.")) + self.cea_source_8.setItemText(1, _translate("ClassicalExtrasOptionsPage", "album_soloists, album_conductors, album_ensembles")) + self.cea_source_8.setItemText(2, _translate("ClassicalExtrasOptionsPage", "soloists, conductors, ensembles, album_composers, composers")) + self.cea_source_8.setItemText(3, _translate("ClassicalExtrasOptionsPage", "album_soloists")) + self.cea_source_8.setItemText(4, _translate("ClassicalExtrasOptionsPage", "album_conductors")) + self.cea_source_8.setItemText(5, _translate("ClassicalExtrasOptionsPage", "album_ensembles")) + self.cea_source_8.setItemText(6, _translate("ClassicalExtrasOptionsPage", "album_composers")) + self.cea_source_8.setItemText(7, _translate("ClassicalExtrasOptionsPage", "album_composer_lastnames")) + self.cea_source_8.setItemText(8, _translate("ClassicalExtrasOptionsPage", "soloists")) + self.cea_source_8.setItemText(9, _translate("ClassicalExtrasOptionsPage", "soloist_names")) + self.cea_source_8.setItemText(10, _translate("ClassicalExtrasOptionsPage", "ensembles")) + self.cea_source_8.setItemText(11, _translate("ClassicalExtrasOptionsPage", "ensemble_names")) + self.cea_source_8.setItemText(12, _translate("ClassicalExtrasOptionsPage", "composers")) + self.cea_source_8.setItemText(13, _translate("ClassicalExtrasOptionsPage", "arrangers")) + self.cea_source_8.setItemText(14, _translate("ClassicalExtrasOptionsPage", "orchestrators")) + self.cea_source_8.setItemText(15, _translate("ClassicalExtrasOptionsPage", "conductors")) + self.cea_source_8.setItemText(16, _translate("ClassicalExtrasOptionsPage", "chorusmasters")) + self.cea_source_8.setItemText(17, _translate("ClassicalExtrasOptionsPage", "leaders")) + self.cea_source_8.setItemText(18, _translate("ClassicalExtrasOptionsPage", "support_performers")) + self.cea_source_8.setItemText(19, _translate("ClassicalExtrasOptionsPage", "work_type")) + self.cea_source_8.setItemText(20, _translate("ClassicalExtrasOptionsPage", "release")) + self.label_35.setText(_translate("ClassicalExtrasOptionsPage", "into tags:")) + self.cea_tag_8.setToolTip(_translate("ClassicalExtrasOptionsPage", "Enter comma-separated list of tags")) + self.cea_cond_8.setText(_translate("ClassicalExtrasOptionsPage", "Conditional?")) + self.toolButton_9.setToolTip(_translate("ClassicalExtrasOptionsPage", "Click to edit sources")) + self.toolButton_9.setText(_translate("ClassicalExtrasOptionsPage", "Source from:")) + self.cea_source_9.setToolTip(_translate("ClassicalExtrasOptionsPage", "Click button to edit. See notes above.")) + self.cea_source_9.setItemText(1, _translate("ClassicalExtrasOptionsPage", "album_soloists, album_conductors, album_ensembles")) + self.cea_source_9.setItemText(2, _translate("ClassicalExtrasOptionsPage", "soloists, conductors, ensembles, album_composers, composers")) + self.cea_source_9.setItemText(3, _translate("ClassicalExtrasOptionsPage", "album_soloists")) + self.cea_source_9.setItemText(4, _translate("ClassicalExtrasOptionsPage", "album_conductors")) + self.cea_source_9.setItemText(5, _translate("ClassicalExtrasOptionsPage", "album_ensembles")) + self.cea_source_9.setItemText(6, _translate("ClassicalExtrasOptionsPage", "album_composers")) + self.cea_source_9.setItemText(7, _translate("ClassicalExtrasOptionsPage", "album_composer_lastnames")) + self.cea_source_9.setItemText(8, _translate("ClassicalExtrasOptionsPage", "soloists")) + self.cea_source_9.setItemText(9, _translate("ClassicalExtrasOptionsPage", "soloist_names")) + self.cea_source_9.setItemText(10, _translate("ClassicalExtrasOptionsPage", "ensembles")) + self.cea_source_9.setItemText(11, _translate("ClassicalExtrasOptionsPage", "ensemble_names")) + self.cea_source_9.setItemText(12, _translate("ClassicalExtrasOptionsPage", "composers")) + self.cea_source_9.setItemText(13, _translate("ClassicalExtrasOptionsPage", "arrangers")) + self.cea_source_9.setItemText(14, _translate("ClassicalExtrasOptionsPage", "orchestrators")) + self.cea_source_9.setItemText(15, _translate("ClassicalExtrasOptionsPage", "conductors")) + self.cea_source_9.setItemText(16, _translate("ClassicalExtrasOptionsPage", "chorusmasters")) + self.cea_source_9.setItemText(17, _translate("ClassicalExtrasOptionsPage", "leaders")) + self.cea_source_9.setItemText(18, _translate("ClassicalExtrasOptionsPage", "support_performers")) + self.cea_source_9.setItemText(19, _translate("ClassicalExtrasOptionsPage", "work_type")) + self.cea_source_9.setItemText(20, _translate("ClassicalExtrasOptionsPage", "release")) + self.label_53.setText(_translate("ClassicalExtrasOptionsPage", "into tags:")) + self.cea_tag_9.setToolTip(_translate("ClassicalExtrasOptionsPage", "Enter comma-separated list of tags")) + self.cea_cond_9.setText(_translate("ClassicalExtrasOptionsPage", "Conditional?")) + self.toolButton_10.setToolTip(_translate("ClassicalExtrasOptionsPage", "Click to edit sources")) + self.toolButton_10.setText(_translate("ClassicalExtrasOptionsPage", "Source from:")) + self.cea_source_10.setToolTip(_translate("ClassicalExtrasOptionsPage", "Click button to edit. See notes above.")) + self.cea_source_10.setItemText(1, _translate("ClassicalExtrasOptionsPage", "album_soloists, album_conductors, album_ensembles")) + self.cea_source_10.setItemText(2, _translate("ClassicalExtrasOptionsPage", "soloists, conductors, ensembles, album_composers, composers")) + self.cea_source_10.setItemText(3, _translate("ClassicalExtrasOptionsPage", "album_soloists")) + self.cea_source_10.setItemText(4, _translate("ClassicalExtrasOptionsPage", "album_conductors")) + self.cea_source_10.setItemText(5, _translate("ClassicalExtrasOptionsPage", "album_ensembles")) + self.cea_source_10.setItemText(6, _translate("ClassicalExtrasOptionsPage", "album_composers")) + self.cea_source_10.setItemText(7, _translate("ClassicalExtrasOptionsPage", "album_composer_lastnames")) + self.cea_source_10.setItemText(8, _translate("ClassicalExtrasOptionsPage", "soloists")) + self.cea_source_10.setItemText(9, _translate("ClassicalExtrasOptionsPage", "soloist_names")) + self.cea_source_10.setItemText(10, _translate("ClassicalExtrasOptionsPage", "ensembles")) + self.cea_source_10.setItemText(11, _translate("ClassicalExtrasOptionsPage", "ensemble_names")) + self.cea_source_10.setItemText(12, _translate("ClassicalExtrasOptionsPage", "composers")) + self.cea_source_10.setItemText(13, _translate("ClassicalExtrasOptionsPage", "arrangers")) + self.cea_source_10.setItemText(14, _translate("ClassicalExtrasOptionsPage", "orchestrators")) + self.cea_source_10.setItemText(15, _translate("ClassicalExtrasOptionsPage", "conductors")) + self.cea_source_10.setItemText(16, _translate("ClassicalExtrasOptionsPage", "chorusmasters")) + self.cea_source_10.setItemText(17, _translate("ClassicalExtrasOptionsPage", "leaders")) + self.cea_source_10.setItemText(18, _translate("ClassicalExtrasOptionsPage", "support_performers")) + self.cea_source_10.setItemText(19, _translate("ClassicalExtrasOptionsPage", "work_type")) + self.cea_source_10.setItemText(20, _translate("ClassicalExtrasOptionsPage", "release")) + self.label_55.setText(_translate("ClassicalExtrasOptionsPage", "into tags:")) + self.cea_tag_10.setToolTip(_translate("ClassicalExtrasOptionsPage", "Enter comma-separated list of tags")) + self.cea_cond_10.setText(_translate("ClassicalExtrasOptionsPage", "Conditional?")) + self.toolButton_11.setToolTip(_translate("ClassicalExtrasOptionsPage", "Click to edit sources")) + self.toolButton_11.setText(_translate("ClassicalExtrasOptionsPage", "Source from:")) + self.cea_source_11.setToolTip(_translate("ClassicalExtrasOptionsPage", "Click button to edit. See notes above.")) + self.cea_source_11.setItemText(1, _translate("ClassicalExtrasOptionsPage", "album_soloists, album_conductors, album_ensembles")) + self.cea_source_11.setItemText(2, _translate("ClassicalExtrasOptionsPage", "soloists, conductors, ensembles, album_composers, composers")) + self.cea_source_11.setItemText(3, _translate("ClassicalExtrasOptionsPage", "album_soloists")) + self.cea_source_11.setItemText(4, _translate("ClassicalExtrasOptionsPage", "album_conductors")) + self.cea_source_11.setItemText(5, _translate("ClassicalExtrasOptionsPage", "album_ensembles")) + self.cea_source_11.setItemText(6, _translate("ClassicalExtrasOptionsPage", "album_composers")) + self.cea_source_11.setItemText(7, _translate("ClassicalExtrasOptionsPage", "album_composer_lastnames")) + self.cea_source_11.setItemText(8, _translate("ClassicalExtrasOptionsPage", "soloists")) + self.cea_source_11.setItemText(9, _translate("ClassicalExtrasOptionsPage", "soloist_names")) + self.cea_source_11.setItemText(10, _translate("ClassicalExtrasOptionsPage", "ensembles")) + self.cea_source_11.setItemText(11, _translate("ClassicalExtrasOptionsPage", "ensemble_names")) + self.cea_source_11.setItemText(12, _translate("ClassicalExtrasOptionsPage", "composers")) + self.cea_source_11.setItemText(13, _translate("ClassicalExtrasOptionsPage", "arrangers")) + self.cea_source_11.setItemText(14, _translate("ClassicalExtrasOptionsPage", "orchestrators")) + self.cea_source_11.setItemText(15, _translate("ClassicalExtrasOptionsPage", "conductors")) + self.cea_source_11.setItemText(16, _translate("ClassicalExtrasOptionsPage", "chorusmasters")) + self.cea_source_11.setItemText(17, _translate("ClassicalExtrasOptionsPage", "leaders")) + self.cea_source_11.setItemText(18, _translate("ClassicalExtrasOptionsPage", "support_performers")) + self.cea_source_11.setItemText(19, _translate("ClassicalExtrasOptionsPage", "work_type")) + self.cea_source_11.setItemText(20, _translate("ClassicalExtrasOptionsPage", "release")) + self.label_57.setText(_translate("ClassicalExtrasOptionsPage", "into tags:")) + self.cea_tag_11.setToolTip(_translate("ClassicalExtrasOptionsPage", "Enter comma-separated list of tags")) + self.cea_cond_11.setText(_translate("ClassicalExtrasOptionsPage", "Conditional?")) + self.toolButton_12.setToolTip(_translate("ClassicalExtrasOptionsPage", "Click to edit sources")) + self.toolButton_12.setText(_translate("ClassicalExtrasOptionsPage", "Source from:")) + self.cea_source_12.setToolTip(_translate("ClassicalExtrasOptionsPage", "Click button to edit. See notes above.")) + self.cea_source_12.setItemText(1, _translate("ClassicalExtrasOptionsPage", "album_soloists, album_conductors, album_ensembles")) + self.cea_source_12.setItemText(2, _translate("ClassicalExtrasOptionsPage", "soloists, conductors, ensembles, album_composers, composers")) + self.cea_source_12.setItemText(3, _translate("ClassicalExtrasOptionsPage", "album_soloists")) + self.cea_source_12.setItemText(4, _translate("ClassicalExtrasOptionsPage", "album_conductors")) + self.cea_source_12.setItemText(5, _translate("ClassicalExtrasOptionsPage", "album_ensembles")) + self.cea_source_12.setItemText(6, _translate("ClassicalExtrasOptionsPage", "album_composers")) + self.cea_source_12.setItemText(7, _translate("ClassicalExtrasOptionsPage", "album_composer_lastnames")) + self.cea_source_12.setItemText(8, _translate("ClassicalExtrasOptionsPage", "soloists")) + self.cea_source_12.setItemText(9, _translate("ClassicalExtrasOptionsPage", "soloist_names")) + self.cea_source_12.setItemText(10, _translate("ClassicalExtrasOptionsPage", "ensembles")) + self.cea_source_12.setItemText(11, _translate("ClassicalExtrasOptionsPage", "ensemble_names")) + self.cea_source_12.setItemText(12, _translate("ClassicalExtrasOptionsPage", "composers")) + self.cea_source_12.setItemText(13, _translate("ClassicalExtrasOptionsPage", "arrangers")) + self.cea_source_12.setItemText(14, _translate("ClassicalExtrasOptionsPage", "orchestrators")) + self.cea_source_12.setItemText(15, _translate("ClassicalExtrasOptionsPage", "conductors")) + self.cea_source_12.setItemText(16, _translate("ClassicalExtrasOptionsPage", "chorusmasters")) + self.cea_source_12.setItemText(17, _translate("ClassicalExtrasOptionsPage", "leaders")) + self.cea_source_12.setItemText(18, _translate("ClassicalExtrasOptionsPage", "support_performers")) + self.cea_source_12.setItemText(19, _translate("ClassicalExtrasOptionsPage", "work_type")) + self.cea_source_12.setItemText(20, _translate("ClassicalExtrasOptionsPage", "release")) + self.label_59.setText(_translate("ClassicalExtrasOptionsPage", "into tags:")) + self.cea_tag_12.setToolTip(_translate("ClassicalExtrasOptionsPage", "Enter comma-separated list of tags")) + self.cea_cond_12.setText(_translate("ClassicalExtrasOptionsPage", "Conditional?")) + self.toolButton_13.setToolTip(_translate("ClassicalExtrasOptionsPage", "Click to edit sources")) + self.toolButton_13.setText(_translate("ClassicalExtrasOptionsPage", "Source from:")) + self.cea_source_13.setToolTip(_translate("ClassicalExtrasOptionsPage", "Click button to edit. See notes above.")) + self.cea_source_13.setItemText(1, _translate("ClassicalExtrasOptionsPage", "album_soloists, album_conductors, album_ensembles")) + self.cea_source_13.setItemText(2, _translate("ClassicalExtrasOptionsPage", "soloists, conductors, ensembles, album_composers, composers")) + self.cea_source_13.setItemText(3, _translate("ClassicalExtrasOptionsPage", "album_soloists")) + self.cea_source_13.setItemText(4, _translate("ClassicalExtrasOptionsPage", "album_conductors")) + self.cea_source_13.setItemText(5, _translate("ClassicalExtrasOptionsPage", "album_ensembles")) + self.cea_source_13.setItemText(6, _translate("ClassicalExtrasOptionsPage", "album_composers")) + self.cea_source_13.setItemText(7, _translate("ClassicalExtrasOptionsPage", "album_composer_lastnames")) + self.cea_source_13.setItemText(8, _translate("ClassicalExtrasOptionsPage", "soloists")) + self.cea_source_13.setItemText(9, _translate("ClassicalExtrasOptionsPage", "soloist_names")) + self.cea_source_13.setItemText(10, _translate("ClassicalExtrasOptionsPage", "ensembles")) + self.cea_source_13.setItemText(11, _translate("ClassicalExtrasOptionsPage", "ensemble_names")) + self.cea_source_13.setItemText(12, _translate("ClassicalExtrasOptionsPage", "composers")) + self.cea_source_13.setItemText(13, _translate("ClassicalExtrasOptionsPage", "arrangers")) + self.cea_source_13.setItemText(14, _translate("ClassicalExtrasOptionsPage", "orchestrators")) + self.cea_source_13.setItemText(15, _translate("ClassicalExtrasOptionsPage", "conductors")) + self.cea_source_13.setItemText(16, _translate("ClassicalExtrasOptionsPage", "chorusmasters")) + self.cea_source_13.setItemText(17, _translate("ClassicalExtrasOptionsPage", "leaders")) + self.cea_source_13.setItemText(18, _translate("ClassicalExtrasOptionsPage", "support_performers")) + self.cea_source_13.setItemText(19, _translate("ClassicalExtrasOptionsPage", "work_type")) + self.cea_source_13.setItemText(20, _translate("ClassicalExtrasOptionsPage", "release")) + self.label_61.setText(_translate("ClassicalExtrasOptionsPage", "into tags:")) + self.cea_tag_13.setToolTip(_translate("ClassicalExtrasOptionsPage", "Enter comma-separated list of tags")) + self.cea_cond_13.setText(_translate("ClassicalExtrasOptionsPage", "Conditional?")) + self.toolButton_14.setToolTip(_translate("ClassicalExtrasOptionsPage", "Click to edit sources")) + self.toolButton_14.setText(_translate("ClassicalExtrasOptionsPage", "Source from:")) + self.cea_source_14.setToolTip(_translate("ClassicalExtrasOptionsPage", "Click button to edit. See notes above.")) + self.cea_source_14.setItemText(1, _translate("ClassicalExtrasOptionsPage", "album_soloists, album_conductors, album_ensembles")) + self.cea_source_14.setItemText(2, _translate("ClassicalExtrasOptionsPage", "soloists, conductors, ensembles, album_composers, composers")) + self.cea_source_14.setItemText(3, _translate("ClassicalExtrasOptionsPage", "album_soloists")) + self.cea_source_14.setItemText(4, _translate("ClassicalExtrasOptionsPage", "album_conductors")) + self.cea_source_14.setItemText(5, _translate("ClassicalExtrasOptionsPage", "album_ensembles")) + self.cea_source_14.setItemText(6, _translate("ClassicalExtrasOptionsPage", "album_composers")) + self.cea_source_14.setItemText(7, _translate("ClassicalExtrasOptionsPage", "album_composer_lastnames")) + self.cea_source_14.setItemText(8, _translate("ClassicalExtrasOptionsPage", "soloists")) + self.cea_source_14.setItemText(9, _translate("ClassicalExtrasOptionsPage", "soloist_names")) + self.cea_source_14.setItemText(10, _translate("ClassicalExtrasOptionsPage", "ensembles")) + self.cea_source_14.setItemText(11, _translate("ClassicalExtrasOptionsPage", "ensemble_names")) + self.cea_source_14.setItemText(12, _translate("ClassicalExtrasOptionsPage", "composers")) + self.cea_source_14.setItemText(13, _translate("ClassicalExtrasOptionsPage", "arrangers")) + self.cea_source_14.setItemText(14, _translate("ClassicalExtrasOptionsPage", "orchestrators")) + self.cea_source_14.setItemText(15, _translate("ClassicalExtrasOptionsPage", "conductors")) + self.cea_source_14.setItemText(16, _translate("ClassicalExtrasOptionsPage", "chorusmasters")) + self.cea_source_14.setItemText(17, _translate("ClassicalExtrasOptionsPage", "leaders")) + self.cea_source_14.setItemText(18, _translate("ClassicalExtrasOptionsPage", "support_performers")) + self.cea_source_14.setItemText(19, _translate("ClassicalExtrasOptionsPage", "work_type")) + self.cea_source_14.setItemText(20, _translate("ClassicalExtrasOptionsPage", "release")) + self.label_63.setText(_translate("ClassicalExtrasOptionsPage", "into tags:")) + self.cea_tag_14.setToolTip(_translate("ClassicalExtrasOptionsPage", "Enter comma-separated list of tags")) + self.cea_cond_14.setText(_translate("ClassicalExtrasOptionsPage", "Conditional?")) + self.toolButton_15.setToolTip(_translate("ClassicalExtrasOptionsPage", "Click to edit sources")) + self.toolButton_15.setText(_translate("ClassicalExtrasOptionsPage", "Source from:")) + self.cea_source_15.setToolTip(_translate("ClassicalExtrasOptionsPage", "Click button to edit. See notes above.")) + self.cea_source_15.setItemText(1, _translate("ClassicalExtrasOptionsPage", "album_soloists, album_conductors, album_ensembles")) + self.cea_source_15.setItemText(2, _translate("ClassicalExtrasOptionsPage", "soloists, conductors, ensembles, album_composers, composers")) + self.cea_source_15.setItemText(3, _translate("ClassicalExtrasOptionsPage", "album_soloists")) + self.cea_source_15.setItemText(4, _translate("ClassicalExtrasOptionsPage", "album_conductors")) + self.cea_source_15.setItemText(5, _translate("ClassicalExtrasOptionsPage", "album_ensembles")) + self.cea_source_15.setItemText(6, _translate("ClassicalExtrasOptionsPage", "album_composers")) + self.cea_source_15.setItemText(7, _translate("ClassicalExtrasOptionsPage", "album_composer_lastnames")) + self.cea_source_15.setItemText(8, _translate("ClassicalExtrasOptionsPage", "soloists")) + self.cea_source_15.setItemText(9, _translate("ClassicalExtrasOptionsPage", "soloist_names")) + self.cea_source_15.setItemText(10, _translate("ClassicalExtrasOptionsPage", "ensembles")) + self.cea_source_15.setItemText(11, _translate("ClassicalExtrasOptionsPage", "ensemble_names")) + self.cea_source_15.setItemText(12, _translate("ClassicalExtrasOptionsPage", "composers")) + self.cea_source_15.setItemText(13, _translate("ClassicalExtrasOptionsPage", "arrangers")) + self.cea_source_15.setItemText(14, _translate("ClassicalExtrasOptionsPage", "orchestrators")) + self.cea_source_15.setItemText(15, _translate("ClassicalExtrasOptionsPage", "conductors")) + self.cea_source_15.setItemText(16, _translate("ClassicalExtrasOptionsPage", "chorusmasters")) + self.cea_source_15.setItemText(17, _translate("ClassicalExtrasOptionsPage", "leaders")) + self.cea_source_15.setItemText(18, _translate("ClassicalExtrasOptionsPage", "support_performers")) + self.cea_source_15.setItemText(19, _translate("ClassicalExtrasOptionsPage", "work_type")) + self.cea_source_15.setItemText(20, _translate("ClassicalExtrasOptionsPage", "release")) + self.label_65.setText(_translate("ClassicalExtrasOptionsPage", "into tags:")) + self.cea_tag_15.setToolTip(_translate("ClassicalExtrasOptionsPage", "Enter comma-separated list of tags")) + self.cea_cond_15.setText(_translate("ClassicalExtrasOptionsPage", "Conditional?")) + self.toolButton_16.setToolTip(_translate("ClassicalExtrasOptionsPage", "Click to edit sources")) + self.toolButton_16.setText(_translate("ClassicalExtrasOptionsPage", "Source from:")) + self.cea_source_16.setToolTip(_translate("ClassicalExtrasOptionsPage", "Click button to edit. See notes above.")) + self.cea_source_16.setItemText(1, _translate("ClassicalExtrasOptionsPage", "album_soloists, album_conductors, album_ensembles")) + self.cea_source_16.setItemText(2, _translate("ClassicalExtrasOptionsPage", "soloists, conductors, ensembles, album_composers, composers")) + self.cea_source_16.setItemText(3, _translate("ClassicalExtrasOptionsPage", "album_soloists")) + self.cea_source_16.setItemText(4, _translate("ClassicalExtrasOptionsPage", "album_conductors")) + self.cea_source_16.setItemText(5, _translate("ClassicalExtrasOptionsPage", "album_ensembles")) + self.cea_source_16.setItemText(6, _translate("ClassicalExtrasOptionsPage", "album_composers")) + self.cea_source_16.setItemText(7, _translate("ClassicalExtrasOptionsPage", "album_composer_lastnames")) + self.cea_source_16.setItemText(8, _translate("ClassicalExtrasOptionsPage", "soloists")) + self.cea_source_16.setItemText(9, _translate("ClassicalExtrasOptionsPage", "soloist_names")) + self.cea_source_16.setItemText(10, _translate("ClassicalExtrasOptionsPage", "ensembles")) + self.cea_source_16.setItemText(11, _translate("ClassicalExtrasOptionsPage", "ensemble_names")) + self.cea_source_16.setItemText(12, _translate("ClassicalExtrasOptionsPage", "composers")) + self.cea_source_16.setItemText(13, _translate("ClassicalExtrasOptionsPage", "arrangers")) + self.cea_source_16.setItemText(14, _translate("ClassicalExtrasOptionsPage", "orchestrators")) + self.cea_source_16.setItemText(15, _translate("ClassicalExtrasOptionsPage", "conductors")) + self.cea_source_16.setItemText(16, _translate("ClassicalExtrasOptionsPage", "chorusmasters")) + self.cea_source_16.setItemText(17, _translate("ClassicalExtrasOptionsPage", "leaders")) + self.cea_source_16.setItemText(18, _translate("ClassicalExtrasOptionsPage", "support_performers")) + self.cea_source_16.setItemText(19, _translate("ClassicalExtrasOptionsPage", "work_type")) + self.cea_source_16.setItemText(20, _translate("ClassicalExtrasOptionsPage", "release")) + self.label_67.setText(_translate("ClassicalExtrasOptionsPage", "into tags:")) + self.cea_tag_16.setToolTip(_translate("ClassicalExtrasOptionsPage", "Enter comma-separated list of tags")) + self.cea_cond_16.setText(_translate("ClassicalExtrasOptionsPage", "Conditional?")) + self.label_42.setText(_translate("ClassicalExtrasOptionsPage", "(If source is empty, tag will be left unchanged) ")) + self.label_17.setText(_translate("ClassicalExtrasOptionsPage", "(Conditional tags will only be filled if previously empty)")) + self.cea_tag_sort.setToolTip(_translate("ClassicalExtrasOptionsPage", "Select to include sort-tags, where available. See "What\'s this?" for more details.
")) + self.cea_tag_sort.setWhatsThis(_translate("ClassicalExtrasOptionsPage", "If a sort tag is associated with the source tag then the sort names will be placed in a sort tag corresponding to the destination tag. Note that the only explicit sort tags written by Picard are for artist, albumartist and composer. Piacrd also writes hidden variables \'_artists_sort\' and \'albumartists_sort\' (note the plurals - these are the sort tags for multi-valued alternatives \'artists\' and \'_albumartists\'). To be consistent with this approach, the plugin writes hidden variables for other tags - e.g. \'_arranger_sort\'. The plugin also writes hidden sort variables for the various hidden artist variables - e.g. \'_cwp_librettists\' has a matching sort variable \'_cwp_librettists_sort\'. Therefore most artist-type sources will have a sort tag/variable associated with them and these will be placed in a destination sort tag if this option is selected - in other words, selecting this option will cause most destination tags to have associated sort tags. Furthermore, any hidden sort variables associated with tags which are not listed explicitly in the tag mapping section will also be written out as tags (i.e. even if the related tags are not included as destination tags). Note, however, that composite sources (e.g. " ensemble_names + \\; + conductors") do not have sort tags associated with them.
If this option is not selected, no additional sort tags will be written, but the hidden variables will still be available, so if a sort tag is required explicitly, just map the sort tag directly - e.g. map \'conductors_sort\' to \'conductor_sort\'.
")) + self.cea_tag_sort.setText(_translate("ClassicalExtrasOptionsPage", "Also populate sort tags")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.Tag_mapping), _translate("ClassicalExtrasOptionsPage", "Tag mapping")) + self.label_110.setText(_translate("ClassicalExtrasOptionsPage", "General
")) + self.ce_no_run.setText(_translate("ClassicalExtrasOptionsPage", "Do not run Classical Extras for tracks where no pre-existing file is detected (warning tag will be written)")) + self.advanced_artists_label.setText(_translate("ClassicalExtrasOptionsPage", "Artists (only effective if \"Artists\" section enabled)
")) + self.advanced_artists_box.setToolTip(_translate("ClassicalExtrasOptionsPage", "Separate multiple names by commas. Do not use any quotation marks.
")) + self.advanced_artists_box.setWhatsThis(_translate("ClassicalExtrasOptionsPage", "Permits the listing of strings by which ensembles of different types may be identified. This is used by the plugin to place performer details in the relevant hidden variables and thus make them available for use in the "Tag mapping" tab as sources for any required tags.
If it is important that only whole words are to be matched, be sure to include a space after the string.
")) + self.ensemble_strings_label.setText(_translate("ClassicalExtrasOptionsPage", "Ensemble strings (separate names by commas)
")) + self.cea_orchestras_2.setText(_translate("ClassicalExtrasOptionsPage", "Orchestras")) + self.cea_choirs_2.setText(_translate("ClassicalExtrasOptionsPage", "Choirs")) + self.cea_groups_2.setText(_translate("ClassicalExtrasOptionsPage", "Groups (i.e. other ensembles such as quartets etc.)")) + self.advanced_workparts_label.setText(_translate("ClassicalExtrasOptionsPage", "Works and parts (only effective if \"Works and parts\" section enabled)
")) + self.label_4.setWhatsThis(_translate("ClassicalExtrasOptionsPage", "Sometimes MB lookups fail. Unfortunately Picard (currently) has no automatic "retry" function. The plugin will attempt to retry for the specified number of attempts. If it still fails, the hidden variable _cwp_error will be set with a message; if error logging is checked in section 4, an error message will be written to the log and the contents of _cwp_error will be written out to a special tag called "An_error_has_occurred" which should appear prominently in the bottom pane of Picard. The problem may be resolved by refreshing, otherwise there may be a problem with the MB database availability. It is unlikely to be a software problem with the plugin.
")) + self.label_4.setText(_translate("ClassicalExtrasOptionsPage", "Max number of re-tries to access works (in case of server errors)*
")) + self.label_120.setText(_translate("ClassicalExtrasOptionsPage", "Allow blank part names for arrangements and part recordings if arrangement/partial label is provided
")) + self.label_114.setText(_translate("ClassicalExtrasOptionsPage", "Removal of common text between parent and child works
")) + self.label_90.setText(_translate("ClassicalExtrasOptionsPage", "Minimum number of similar words required before eliminating. Use zero for no elimination.
(Punctuation and accents etc. will be ignored in word comparison)
NB Parent name text at the start of a work which is followed by punctuation in the work name will always be stripped regardless of this setting.
Synonyms in the next section also apply.
This subsection contains various parameters affecting the processing of strings in titles. Because titles are free-form, not all circumstances can be anticipated. Detailed documentation of these is beyond the scope of this Readme as the effects can be quite complex and subtle and may require an understanding of the plugin code (which is of course open-source) to acsertain them. If pure canonical works are used ("Use only metadata from canonical works" and, if necessary, "Full MusicBrainz work hierarchy" on the Works and parts tab, section 2) then this processing should be irrelevant, but no text from titles will be included. Some explanations are given below:
"Proximity of new words". When using extended metadata - i.e. "metadata enhanced with title text", the plugin will attempt to remove similar words between the canonical work name (in MusicBrainz) and the title before extending the canonical name. After removing such words, a rather "bitty" result may occur. To avoid this, any new words with the specified proximity will have the words between them (or up to the end) included even if they repeat words in the work name.
"Prefixes". When using "metadata from titles" or extended metadata, the structure of the works in MusicBrainz is used to infer the structure in the title text, so strings that are repeated between tracks which are part of the same MusicBrainz work will be treated as "higher level". This can lead to anomolies if, for instance, the titles are "Work name: Part 1", "Work name: Part 2", "Part" will be treated as part of the parent work name. Specifying such words in "Prefixes" will prevent this.
"Synonyms". These words will be considered equivalent when comparing work name and title text. Thus if one word appears in the work name, that and its synonym will be removed from the title in extending the metadata (subject to the proximity setting above).
"Replacements". These words/phrases will be replaced in the title text in extended metadata, regardless of the text in the work name.
")) + self.label_115.setText(_translate("ClassicalExtrasOptionsPage", "How title metadata should be included in extended metadata (use cautiously - read documentation)
(Mostly only applies if "Use canonical work metadata enhanced with title text" selected on "Works and parts" tab. However synonyms also apply to parent/child text removal.)
Proximity of new words (to each other) to trigger in-fill with existing words (default = 2)
")) + self.label_7.setText(_translate("ClassicalExtrasOptionsPage", "Proximity of new words (to start or end) to trigger in-fill with existing words (default =1)
")) + self.label_5.setText(_translate("ClassicalExtrasOptionsPage", "Treat hyphenated words as two words for comparison purposes
")) + self.label_93.setText(_translate("ClassicalExtrasOptionsPage", "Proportion of a string to be matched to a (usually larger) string for it to be considered essentially similar (default = 66%)
")) + self.cwp_substring_match.setSuffix(_translate("ClassicalExtrasOptionsPage", "%")) + self.label_87.setText(_translate("ClassicalExtrasOptionsPage", "\n" +"\n" +"Fill part name with title text if it would otherwise have no text other than arrangement or partial annotations
")) + self.label_92.setText(_translate("ClassicalExtrasOptionsPage", "Prepositions/conjunctions and prefixes
DO NOT USE ANY COMMAS OR QUOTE MARKS (apostophes in words are acceptable)
Prepositions & conjunctions: these are words that will not be regarded as providing additional information (not treated as \'new\' words) unless they precede a new word.
Use lower case only, comma separated
Prefixes to be ignored in comparison (case insensitive, comma separated)
To prevent a prefix from being ignored when extending metadata with title info, precede it with a space.
To ensure only whole words are removed, follow with a space.
Separate multiple names by commas. Do not use any quotation marks.
")) + self.cwp_synonyms_2.setText(_translate("ClassicalExtrasOptionsPage", "Synonyms and replacements - must be written as tuples separated by forward slashes - e.g (a,b) / (c,d,e) - a tuple may have two or more synonyms.
N.B. The matching is case-insensitive. Roman numerals will be treated as synonyms of arabic numerals in any event, so no need to enter these.
The last member of the tuple should be the canonical form (i.e. the one to which others are converted for comparison or replacement) and must be a normal string (not a regex). See readme for full details.
Unless entering a regular expression, use backslash \\ to escape any regex metacharacters, namely \\ ^ $ . | ? * + ( ) [ ] {
Also escape commas , and forward slashes /. Do not enclose strings in quote marks.
Enter SYNONYM tuples below - each item in a tuple will be treated as similar when comparing works/parts and titles. The text in tags will be unaltered.
")) + self.label_16.setText(_translate("ClassicalExtrasOptionsPage", "Enter REMOVALS/REPLACEMENTS below - these will result in the "extended" text in tags being changed
Put the word(s), phrase(s), or regular exprerssion(s) in the first part(s) of the tuple. The replacement text (or nothing - to remove) goes in the last member of the tuple.
N.B. Replacement text will operate BEFORE synonyms are considered.
")) + self.cwp_replacements.setToolTip(_translate("ClassicalExtrasOptionsPage", "Entries must be 2-tuples, e.g. (Replace this, with this). Separate multiple tuples by forward slash. Do not use any quotation marks. Spaces are acceptable. The first item of a tuple may be a regular expression - enclose it with double exclamation marks - e.g.(!!regex here!!, replacement text here).
")) + self.advanced_genres_label.setText(_translate("ClassicalExtrasOptionsPage", "Genres etc. (only required if Muso-specific options are used for genres/periods)
")) + self.label_85.setText(_translate("ClassicalExtrasOptionsPage", "Path to Muso reference database:")) + self.label_84.setText(_translate("ClassicalExtrasOptionsPage", "Name of Muso reference database")) + self.label_86.setText(_translate("ClassicalExtrasOptionsPage", "RESTART PICARD AFTER CHANGING THESE
")) + self.label_117.setText(_translate("ClassicalExtrasOptionsPage", "Logging options*
")) + self.logging_box.setWhatsThis(_translate("ClassicalExtrasOptionsPage", "These options are in addition to the options chosen in Picard\'s "Help->View error/debug log" settings. They only affect messages written by this plugin. To enable debug messages to be shown, the flag needs to be set here and "Debug mode" needs to be turned on in the log. It is strongly advised to keep the "debug" and "info" flags unchecked unless debugging is required as they slow up processing significantly and may even cause Picard to crash on large releases. The "error" and "warning" flags should be left checked, unless it is required to suppress messages written out to tags (the default is to write messages to the tags 001_errors and 002_warnings).
")) + self.log_error.setText(_translate("ClassicalExtrasOptionsPage", "Error")) + self.log_warning.setText(_translate("ClassicalExtrasOptionsPage", "Warning")) + self.log_debug.setText(_translate("ClassicalExtrasOptionsPage", "Debug")) + self.custom_logging_box.setTitle(_translate("ClassicalExtrasOptionsPage", "Custom logging")) + self.log_basic.setText(_translate("ClassicalExtrasOptionsPage", "Basic")) + self.log_info.setText(_translate("ClassicalExtrasOptionsPage", "Full")) + self.label_118.setText(_translate("ClassicalExtrasOptionsPage", "Classical Extras Special Tags
")) + self.save_options_box.setWhatsThis(_translate("ClassicalExtrasOptionsPage", "This can be used so that the user has a record of the version of Classical Extras which generated the tags and which options were selected to achieve the resulting tags. Note that the tags will be blanked first so this will only show the last options used on a particular file. The same tag can be used for both sets of options, resulting in a multi-valued tag.
")) + self.save_options_box.setTitle(_translate("ClassicalExtrasOptionsPage", "Save plugin details and options in a tag?*")) + self.label_41.setText(_translate("ClassicalExtrasOptionsPage", "Tag name for plugin version")) + self.label_36.setText(_translate("ClassicalExtrasOptionsPage", "Tag name for artist/mapping/misc. options")) + self.label_38.setText(_translate("ClassicalExtrasOptionsPage", "Tag name for work/genre options")) + self.override_box.setWhatsThis(_translate("ClassicalExtrasOptionsPage", "If options have previously been saved (see above), selecting these will cause the saved options to be used in preference to the displayed options. The displayed options will not be affected and will be used if no saved options are present. The default is for no over-ride. If artist options over-ride is chosen, then tag map detail options may be included or not in the override.
The last checkbox, "Overwrite options in Options Pages", is for VERY CAREFUL USE ONLY. It will cause any options read from the saved tags (if the relevant box has been ticked) to over-write the options on the plugin Options Page UI. The intended use of this is if for some reason the user\'s preferred options have been erased/reverted to default - by using this option, the previously-used choices from a reliable filed album can be used to populate the Options Page. The box will automatically be unticked after loading/refreshing one album, to prevent inadvertant use. Far better is to make a backup copy of the picard.ini file.
")) + self.override_box.setTitle(_translate("ClassicalExtrasOptionsPage", "Over-ride plugin options displayed in Options Pages with options from local file tags (previously saved using method in box above)?*")) + self.cea_override.setText(_translate("ClassicalExtrasOptionsPage", "Artist options")) + self.cwp_override.setText(_translate("ClassicalExtrasOptionsPage", "Work options")) + self.ce_genres_override.setText(_translate("ClassicalExtrasOptionsPage", "Genres etc. options")) + self.ce_tagmap_override.setToolTip(_translate("ClassicalExtrasOptionsPage", "Will not over-ride displayed options unless artist options over-ride is also selected
")) + self.ce_tagmap_override.setText(_translate("ClassicalExtrasOptionsPage", "Tag mapping options")) + self.ce_options_overwrite.setText(_translate("ClassicalExtrasOptionsPage", "Overwrite options in Options Pages (READ WARNINGS in Readme)")) + self.label_121.setText(_translate("ClassicalExtrasOptionsPage", "Note that the above saved options include the related \"advanced\" options on this tab as well as the options on each of the main tabs.")) + self.ce_show_ui_tags.setText(_translate("ClassicalExtrasOptionsPage", "Show additional tags in Picard UI (rhs panel) - N.B. RESTART NEEDED FOR CHANGE TO TAKE EFFECT")) + self.groupBox.setTitle(_translate("ClassicalExtrasOptionsPage", "Additional columns for Picard UI to show specific tags*")) + self.label_94.setText(_translate("ClassicalExtrasOptionsPage", "RESTART PICARD AFTER CHANGING THESE - otherwise changes will not take effect
")) + self.label_8.setText(_translate("ClassicalExtrasOptionsPage", "Notes:
1. Use the format column_name_A: (include_tag_1, include_tag_2) / column_name_B: include_tag_3 etc. (i.e. put multiple tags to be concatenated in brackets)
2. To just flag tags that have changed, rather than show the contents, add _DIFF at the end of the tag name
3. If more than one tag name is included for a column, then:
(a) if the tags are _DIFF tags, then the column will be flagged if any of them have changed
(b) otherwise the tag contents will be concatenated
General description
\n" +"Classical Extras provides tagging enhancements for Picard and, in particular, utilises MusicBrainz’s hierarchy of works to provide work/movement tags. All options are set through a user interface in Picard options->plugins. This interface provides separate sections to enhance artist/performer tags, works and parts, genres and also allows for a generalised "tag mapping" (simple scripting). While it is designed to cater for the complexities of classical music tagging, it may also be useful for other music which has more than just basic song/artist/album data.
The options screen provides five tabs for users to control the tags produced:
1. Artists: Options as to whether artist tags will contain standard MB names, aliases or as-credited names. Ability to include and annotate names for specialist roles (chorus master, arranger, lyricist etc.). Ability to read lyrics tags on the file which has been loaded and assign them to track and album levels if required. (Note: Picard will not normally process incoming file tags).
2. Works and parts: The plugin will build a hierarchy of works and parts (e.g. Work -> Part -> Movement or Opera -> Act -> Number) based on the works in MusicBrainz\'s database. These can then be displayed in tags in a variety of ways according to user preferences. Furthermore partial recordings, medleys, arrangements and collections of works are all handled according to user choices. There is a processing overhead for this at present because MusicBrainz limits look-ups to one per second.
3. Genres etc.: Options are available to customise the source and display of information relating to genres, instruments, keys, work dates and periods. Additional capabilities are provided for users of Muso (or others who provide the relevant XML files) to use pre-existing databases of classical genres, classical composers and classical periods.
4. Tag mapping: in some ways, this is a simple substitute for some of Picard\'s scripting capability. The main advantage is that the plugin will remember what tag mapping you use for each release (or even track).
5. Advanced: Various options to control the detailed processing of the above.
All user options can be saved on a per-album (or even per-track) basis so that tweaks can be used to deal with inconsistencies in the MusicBrainz data (e.g. include English titles from the track listing where the MusicBrainz works are in the composer\'s language and/or script). Also existing file tags can be processed (not possible in native Picard).
Please see my website for full details of this plugin and how to use it.
\n" +"This help page now has only general information.
\n" +"There are extensive tooltips and "What\'s This" popups (right click for them)
")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.Help), _translate("ClassicalExtrasOptionsPage", "Help")) +