Index: SQL/mysql/schema_clear.sql =================================================================== --- SQL/mysql/schema_clear.sql (revision 20488) +++ SQL/mysql/schema_clear.sql (working copy) @@ -3,6 +3,10 @@ -- Use DELETE instead of TRUNCATE, as TRUNCATE seems to need unlocked tables. DELETE FROM tracks; +DELETE FROM tags; + +DELETE FROM tag_track; + DELETE FROM playlist_track; DELETE FROM albums; Index: SQL/mysql/schema_6_up.sql =================================================================== --- SQL/mysql/schema_6_up.sql (revision 0) +++ SQL/mysql/schema_6_up.sql (revision 0) @@ -0,0 +1,36 @@ +-- +-- Table: tags +-- +DROP TABLE IF EXISTS tags; +CREATE TABLE tags ( + id int(10) unsigned NOT NULL auto_increment, + name varchar(40) NOT NULL, + value blob NOT NULL, + valuesort text, + valuesearch text, + customsearch text, + referencetype smallint(10) unsigned, + reference int(10) unsigned, + INDEX tagValueIndex (value(255)), + INDEX tagSortIndex (valuesort(255)), + INDEX tagSearchIndex (valuesearch(255)), + INDEX tagCustomSearchIndex (customsearch(255)), + INDEX tagReferenceIndex (reference), + INDEX tagNameIndex (name), + PRIMARY KEY (id) +) TYPE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci; + +-- +-- Table: genre_track +-- +DROP TABLE IF EXISTS tag_track; +CREATE TABLE tag_track ( + tag int(10) unsigned, + track int(10) unsigned, + INDEX tag_trackTagIndex (tag), + INDEX tag_trackTrackIndex (track), + PRIMARY KEY (tag,track), + FOREIGN KEY (`track`) REFERENCES `tracks` (`id`) ON DELETE CASCADE, + FOREIGN KEY (`tag`) REFERENCES `tags` (`id`) ON DELETE CASCADE +) TYPE=InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci; + Index: SQL/mysql/schema_6_down.sql =================================================================== --- SQL/mysql/schema_6_down.sql (revision 0) +++ SQL/mysql/schema_6_down.sql (revision 0) @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS tag_track; +DROP TABLE IF EXISTS tags; Index: Slim/Schema/Track.pm =================================================================== --- Slim/Schema/Track.pm (revision 20488) +++ Slim/Schema/Track.pm (working copy) @@ -37,6 +37,7 @@ $class->belongs_to('album' => 'Slim::Schema::Album'); $class->has_many('genreTracks' => 'Slim::Schema::GenreTrack' => 'track'); + $class->has_many('tagTracks' => 'Slim::Schema::TagTrack' => 'track'); $class->has_many('comments' => 'Slim::Schema::Comment' => 'track'); $class->has_many('contributorTracks' => 'Slim::Schema::ContributorTrack'); @@ -79,6 +80,12 @@ return $self->genreTracks->search_related('genre', @_); } +sub tags { + my $self = shift; + + return $self->tagTracks->search_related('tag', @_); +} + sub attributes { my $class = shift; Index: Slim/Schema/Tag.pm =================================================================== --- Slim/Schema/Tag.pm (revision 0) +++ Slim/Schema/Tag.pm (revision 0) @@ -0,0 +1,179 @@ +package Slim::Schema::Tag; + +# $Id:$ + +use strict; +use base 'Slim::Schema::DBI'; +use Scalar::Util qw(blessed); + +use Slim::Utils::Misc; + +our %typeOfReference = ( + 'CONTRIBUTOR' => 1, + 'ALBUM' => 2, + 'GENRE' => 3, + 'YEAR' => 4, + 'TAG' => 5, +); + +{ + my $class = __PACKAGE__; + + $class->table('tags'); + + $class->add_columns(qw( + id + name + value + valuesort + valuesearch + referencetype + reference + )); + + $class->set_primary_key('id'); + $class->add_unique_constraint('valuesearch' => [qw/name valuesearch/]); + + $class->has_many('tagTracks' => 'Slim::Schema::TagTrack' => 'tag'); + + if ($] > 5.007) { + $class->utf8_columns(qw/value valuesort/); + } + + $class->resultset_class('Slim::Schema::ResultSet::Tag'); +} + +sub url { + my $self = shift; + + return sprintf('db:tag.valuesearch=%s', Slim::Utils::Misc::escape($self->valuesearch)); +} + +sub tracks { + my $self = shift; + + return $self->tagTracks->search_related('track' => @_); +} + +sub displayAsHTML { + my ($self, $form, $descend, $sort) = @_; + + $form->{'text'} = $self->value; + +# my $Imports = Slim::Music::Import->importers; + +# for my $mixer (keys %{$Imports}) { + +# if (defined $Imports->{$mixer}->{'mixerlink'}) { +# &{$Imports->{$mixer}->{'mixerlink'}}($self, $form, $descend); +# } +# } +} + +sub add { + my $class = shift; + my $tag = shift; + my $track = shift; + my $value = shift; + my $sort = shift; + my $referencetype = shift || $typeOfReference{'TAG'}; + my $reference = shift; + + my @tags = (); + + my @sortValues = (); + if(defined($sort)) { + @sortValues = Slim::Music::Info::splitTag($sort); + } + for my $tagSub (Slim::Music::Info::splitTag($value)) { + + my $valuesort = undef; + unshift @sortValues,$valuesort; + $valuesort = Slim::Utils::Text::ignoreCaseArticles($valuesort || $tagSub); + + # So that ucfirst() works properly. + use locale; + my $tagObj = Slim::Schema->resultset('Tag')->find_or_create({ + 'name' => uc($tag), + 'valuesort' => $valuesort, + 'value' => ucfirst($tagSub), + 'valuesearch' => $valuesort, + 'referencetype' => $referencetype, + 'reference' => $reference, + }, { 'key' => 'valuesearch' }); + if(!$reference && !$tagObj->reference) { + # Set reference to current item for custom tags + $tagObj->set_column('reference',$tagObj->id); + $tagObj->set_column('referencetype',$referencetype); + $tagObj->update(); + }elsif($reference && $referencetype < $typeOfReference{'TAG'} && $tagObj->referencetype >= $typeOfReference{'TAG'}) { + # Standard tags should always override custom tags + $tagObj->set_column('reference',$reference); + $tagObj->set_column('referencetype',$referencetype); + $tagObj->update(); + } + + Slim::Schema->resultset('TagTrack')->find_or_create({ + track => $track->id, + tag => $tagObj->id, + }); + + push @tags, $tagObj; + } + + return wantarray ? @tags : $tags[0]; +} + +sub reference { + my $self = shift; + + return undef if !$self->reference; + + if($self->referencetype == $typeOfReference{'CONTRIBUTOR'}) { + return $self->contributor; + }elsif($self->referencetype == $typeOfReference{'ALBUM'}) { + return $self->album; + }elsif($self->referencetype == $typeOfReference{'GENRE'}) { + return $self->genre; + }elsif($self->referencetype == $typeOfReference{'YEAR'}) { + return $self->year; + }else { + return undef; + } +} + +sub contributor { + my $self = shift; + + return undef if !$self->reference || $self->referencetype != $typeOfReference{'CONTRIBUTOR'}; + + return Slim::Schema::resultset('Contributor')->find($self->reference); +} + +sub album { + my $self = shift; + + return undef if !$self->reference || $self->referencetype != $typeOfReference{'ALBUM'}; + + return Slim::Schema::resultset('Album')->find($self->reference); +} + +sub genre { + my $self = shift; + + return undef if !$self->reference || $self->referencetype != $typeOfReference{'GENRE'}; + + return Slim::Schema::resultset('Genre')->find($self->reference); +} + +sub year { + my $self = shift; + + return undef if !$self->reference || $self->referencetype != $typeOfReference{'YEAR'}; + + return Slim::Schema::resultset('Year')->find($self->reference); +} + +1; + +__END__ Index: Slim/Schema/ResultSet/Tag.pm =================================================================== --- Slim/Schema/ResultSet/Tag.pm (revision 0) +++ Slim/Schema/ResultSet/Tag.pm (revision 0) @@ -0,0 +1,98 @@ +package Slim::Schema::ResultSet::Tag; + +# $Id:$ + +use strict; +use base qw(Slim::Schema::ResultSet::Base); + +use Slim::Utils::Prefs; + +sub pageBarResults { + my $self = shift; + + my $table = $self->{'attrs'}{'alias'}; + my $value = "$table.valuesort"; + + $self->search(undef, { + 'select' => [ \"LEFT($value, 1)", { count => \"DISTINCT($table.id)" } ], + as => [ 'letter', 'count' ], + group_by => \"LEFT($value, 1)", + result_class => 'Slim::Schema::PageBar', + }); +} + +sub title { + my $self = shift; + + return 'BROWSE_BY_TAG'; +} + +sub allTitle { + my $self = shift; + + return 'ALL_TAGS'; +} + +sub alphaPageBar { 1 } + +sub searchColumn { + my $self = shift; + + return 'valuesearch'; +} + +sub searchNames { + my $self = shift; + my $terms = shift; + my $attrs = shift || {}; + + $attrs->{'order_by'} ||= 'me.valuesort'; + $attrs->{'distinct'} ||= 'me.id'; + + return $self->search({ 'me.valuesearch' => { 'like' => $terms } }, $attrs); +} + +sub browse { + my $self = shift; + my $find = shift; + my $cond = shift; + my $sort = shift; + + return $self->search($cond, { 'order_by' => 'me.valuesort' }); +} + +sub descendContributor { + my $self = shift; + my $find = shift; + my $cond = shift; + my $sort = shift; + + # Get our own RS first - then search for related, which builds up a LEFT JOIN query. + my $rs = $self->search($cond)->search_related('tagTracks'); + + # If we are automatically identifiying VA albums, constrain the query. + if (preferences('server')->get('variousArtistAutoIdentification')) { + + $rs = $rs->search_related('track', { + 'album.compilation' => [ { 'is' => undef }, { '=' => 0 } ] + }, { 'join' => 'album' }); + + } else { + + $rs = $rs->search_related('track'); + } + + # The user may not want to include all the composers / conductors + if (my $roles = Slim::Schema->artistOnlyRoles) { + + $rs = $rs->search_related('contributorTracks', { 'contributorTracks.role' => { 'in' => $roles } }); + + } else { + + $rs = $rs->search_related('contributorTracks'); + } + + return $rs->search_related('contributor', {}, { 'order_by' => $sort || 'contributor.namesort' }); +} + +1; Index: Slim/Schema/TagTrack.pm =================================================================== --- Slim/Schema/TagTrack.pm (revision 0) +++ Slim/Schema/TagTrack.pm (revision 0) @@ -0,0 +1,27 @@ +package Slim::Schema::TagTrack; + +# $Id:$ +# +# Tag to track mapping class + +use strict; +use base 'Slim::Schema::DBI'; + +{ + my $class = __PACKAGE__; + + $class->table('tag_track'); + + $class->add_columns(qw/tag track/); + + $class->set_primary_key(qw/tag track/); + $class->add_unique_constraint('tag_track' => [qw/tag track/]); + + $class->belongs_to('tag' => 'Slim::Schema::Tag'); + $class->belongs_to('track' => 'Slim::Schema::Track'); +} + +1; + +__END__ + Index: Slim/Utils/Prefs.pm =================================================================== --- Slim/Utils/Prefs.pm (revision 20488) +++ Slim/Utils/Prefs.pm (working copy) @@ -188,6 +188,23 @@ 'variousArtistAutoIdentification' => 0, 'useBandAsAlbumArtist' => 0, 'variousArtistsString' => undef, + 'scannedStandardTags' => [ + 'ARTIST', + 'COMPOSER', + 'CONDUCTOR', + 'BAND', + 'ALBUMARTIST', + 'TRACKARTIST', + 'ALBUM', + 'YEAR', + 'GENRE', + ], + 'scannedCustomTags' => { + 'PERFORMER' => 'PERFORMERSORT', + 'WORK' => 'WORKSORT', + 'ENSEMBLE' => 'ENSEMBLESORT', + 'OPUS' => 'OPUSSORT', + }, # Server Settings - FileTypes 'disabledextensionsaudio' => '', 'disabledextensionsplaylist' => '', @@ -406,7 +423,7 @@ $prefs->setChange( sub { Slim::Utils::Strings::setLanguage($_[1]) }, 'language' ); $prefs->setChange( \&main::checkVersion, 'checkVersion'); - $prefs->setChange( sub { Slim::Control::Request::executeRequest(undef, ['wipecache']) }, qw(splitList groupdiscs) ); + $prefs->setChange( sub { Slim::Control::Request::executeRequest(undef, ['wipecache']) }, qw(splitList groupdiscs scannedCustomTags scannedStandardTags) ); $prefs->setChange( sub { Slim::Utils::Misc::setPriority($_[1]) }, 'serverPriority'); Index: Slim/Schema.pm =================================================================== --- Slim/Schema.pm (revision 20488) +++ Slim/Schema.pm (working copy) @@ -173,6 +173,8 @@ Playlist PlaylistTrack Rescan + Tag + TagTrack Track Year Progress @@ -821,6 +823,7 @@ return; } + my %allAttributes = %$attributeHash; ($attributeHash, $deferredAttributes) = $self->_preCheckAttributes({ 'url' => $url, 'attributes' => $attributeHash, @@ -884,6 +887,7 @@ $self->_postCheckAttributes({ 'track' => $track, 'attributes' => $deferredAttributes, + 'allattributes' => \%allAttributes, 'create' => 1, }); @@ -954,6 +958,7 @@ my $readTags = $args->{'readTags'} || 0; my $checkMTime = $args->{'checkMTime'}; my $playlist = $args->{'playlist'}; + my %allAttributes = %$attributeHash; # XXX - exception should go here. Comming soon. my ($track, $url, $blessed) = _validTrackOrURL($urlOrObj); @@ -999,6 +1004,7 @@ } my $deferredAttributes; + my %allAttributes = %$attributeHash; ($attributeHash, $deferredAttributes) = $self->_preCheckAttributes({ 'url' => $url, 'attributes' => $attributeHash, @@ -1022,6 +1028,7 @@ $self->_postCheckAttributes({ 'track' => $track, 'attributes' => $deferredAttributes, + 'allattributes' => \%allAttributes, }); } @@ -1815,6 +1822,7 @@ my $track = $args->{'track'}; my $attributes = $args->{'attributes'}; + my $allAttributes = $args->{'allattributes'}; my $create = $args->{'create'} || 0; # Don't bother with directories / lnks. This makes sure "No Artist", @@ -1910,6 +1918,39 @@ $log->debug("-- Track has genre '$genre'"); } + + # Add custom tags + my $customTags = $prefs->get("scannedCustomTags"); + while (my ($key, $val) = each %$customTags) { + + if(exists $allAttributes->{$key}) { + if ($create && $isLocal) { + if(exists $allAttributes->{$val}) { + Slim::Schema::Tag->add($key, $track, $allAttributes->{$key}, $allAttributes->{$val}); + }else { + Slim::Schema::Tag->add($key, $track, $allAttributes->{$key}); + } + + $log->debug(sprintf("-- Track has tag '$key'")); + + } elsif (!$create && $isLocal && $allAttributes->{$key} ne $track->tags->single->name) { + + # Bug 1143: The user has updated the genre tag, and is + # rescanning We need to remove the previous associations. + $track->tagTracks->delete_all; + + if(exists $allAttributes->{$val}) { + Slim::Schema::Tag->add($key, $track, $allAttributes->{$key}, $allAttributes->{$val}); + }else { + Slim::Schema::Tag->add($key, $track, $allAttributes->{$key}); + } + + $log->debug("-- Deleted all previous tags for this track"); + $log->debug("-- Track has tag '$key'"); + } + } + } + # Walk through the valid contributor roles, adding them to the database for each track. my $contributors = $self->_mergeAndCreateContributors($track, $attributes, $isCompilation, $isLocal); my $foundContributor = scalar keys %{$contributors}; @@ -2331,11 +2372,11 @@ # Years have their own lookup table. # Bug: 3911 - don't add years for tracks without albums. my $year = $track->year; - + my $yearObj = undef; if (defined $year && $year =~ /^\d+$/ && $blessedAlbum && $albumObj->title ne string('NO_ALBUM')) { - Slim::Schema->rs('Year')->find_or_create({ 'id' => $year }); + $yearObj = Slim::Schema->rs('Year')->find_or_create({ 'id' => $year }); } # Add comments if we have them: @@ -2349,6 +2390,32 @@ $log->debug("-- Track has comment '$comment'"); } + # Update standard tags + my %standardTags = map { $_ => 1 } @{ $prefs->get('scannedStandardTags') }; + if($albumObj && $standardTags{'ALBUM'}) { + Slim::Schema::Tag->add('ALBUM', $track, $albumObj->title, $albumObj->titlesort,$Slim::Schema::Tag::typeOfReference{'ALBUM'},$albumObj->id); + } + while (my ($role, $contributorList) = each %{$contributors}) { + if($standardTags{$role}) { + for my $contributorObj (@$contributorList) { + Slim::Schema::Tag->add($role, $track, $contributorObj->name, $contributorObj->namesort,$Slim::Schema::Tag::typeOfReference{'CONTRIBUTOR'},$contributorObj->id); + } + } + } + if($standardTags{'GENRE'}) { + my @genres = $track->genres; + for my $genreObj (@genres) { + Slim::Schema::Tag->add('GENRE', $track, $genreObj->name, $genreObj->namesort,$Slim::Schema::Tag::typeOfReference{'GENRE'},$genreObj->id); + } + } + if($standardTags{'YEAR'}) { + if(defined($yearObj)) { + Slim::Schema::Tag->add('YEAR', $track, $yearObj->name, $yearObj->namesort,$Slim::Schema::Tag::typeOfReference{'YEAR'},$yearObj->id); + }else { + Slim::Schema::Tag->add('YEAR', $track, string('UNK'), string('UNK'),$Slim::Schema::Tag::typeOfReference{'YEAR'},0); + } + } + # refcount-- %{$contributors} = (); } Index: Slim/Web/Settings/Server/Behavior.pm =================================================================== --- Slim/Web/Settings/Server/Behavior.pm (revision 20488) +++ Slim/Web/Settings/Server/Behavior.pm (working copy) @@ -12,6 +12,8 @@ use Slim::Utils::Prefs; +my $prefs = preferences('server'); + sub name { return Slim::Web::HTTP::protectName('BEHAVIOR_SETTINGS'); } @@ -28,6 +30,39 @@ ); } +sub handler { + my ($class, $client, $paramRef, $pageSetup) = @_; + + # If this is a settings update + if ($paramRef->{'saveSettings'}) { + + my $scannedCustomTagsString = $paramRef->{'scannedCustomTags'}; + my @scannedCustomTags = split(/\,/,$scannedCustomTagsString); + my %scannedCustomTagsHash = (); + for my $tagEntry (@scannedCustomTags) { + my ($tag,$sortTag) = split(/=/,$tagEntry); + if($tag) { + $sortTag = '' if !defined($sortTag); + $tag =~ s/^\s*//; + $sortTag =~ s/\s*$//; + $scannedCustomTagsHash{uc($tag)}=uc($sortTag); + } + } + $prefs->set('scannedCustomTags',\%scannedCustomTagsHash); + } + my $customTagsDisplayHash = $prefs->get('scannedCustomTags'); + my $customTagsDisplay = ''; + for my $tag (keys %$customTagsDisplayHash) { + if($customTagsDisplay ne '') { + $customTagsDisplay .= ', '; + } + $customTagsDisplay .= ($tag.($customTagsDisplayHash->{$tag}?('='.$customTagsDisplayHash->{$tag}):'')); + } + $paramRef->{'scannedCustomTags'} = $customTagsDisplay; + + return $class->SUPER::handler($client, $paramRef, $pageSetup); +} + 1; __END__ Index: HTML/EN/settings/server/behavior.html =================================================================== --- HTML/EN/settings/server/behavior.html (revision 20488) +++ HTML/EN/settings/server/behavior.html (working copy) @@ -101,5 +101,11 @@ [% END %] + [% WRAPPER settingSection %] + [% WRAPPER settingGroup title="SETUP_SCANNEDCUSTOMTAGS" desc="SETUP_SCANNEDCUSTOMTAGS_DESC"%] + + [% END %] + [% END %] + [% PROCESS settings/footer.html %] Index: strings.txt =================================================================== --- strings.txt (revision 20488) +++ strings.txt (working copy) @@ -10105,6 +10105,9 @@ DA Genre FR Genres +BROWSE_BY_TAG + EN Tags + BROWSE_BY_ARTIST CS Procházet podle interpretů DA Gennemse kunstnere @@ -15831,3 +15834,10 @@ ALBUMS_SORT_METHOD EN Albums Sort Method + +SETUP_SCANNEDCUSTOMTAGS + EN Custom tags + +SETUP_SCANNEDCUSTOMTAGS_DESC + EN Additional custom tags to look for during scanning, entered as a comma separated list of tags as:
TAG1=SORTTAG1, TAG2=SORTTAG2 +