diff --git a/db/migration/20200423204116_add_sort_fields.go b/db/migration/20200423204116_add_sort_fields.go new file mode 100644 index 00000000..db5e9f2c --- /dev/null +++ b/db/migration/20200423204116_add_sort_fields.go @@ -0,0 +1,64 @@ +package migration + +import ( + "database/sql" + "github.com/pressly/goose" +) + +func init() { + goose.AddMigration(Up20200423204116, Down20200423204116) +} + +func Up20200423204116(tx *sql.Tx) error { + _, err := tx.Exec(` +alter table artist + add order_artist_name varchar(255) collate nocase; +alter table artist + add sort_artist_name varchar(255) collate nocase; +create index if not exists artist_order_artist_name + on artist (order_artist_name); + +alter table album + add order_album_name varchar(255) collate nocase; +alter table album + add order_album_artist_name varchar(255) collate nocase; +alter table album + add sort_album_name varchar(255) collate nocase; +alter table album + add sort_artist_name varchar(255) collate nocase; +alter table album + add sort_album_artist_name varchar(255) collate nocase; +create index if not exists album_order_album_name + on album (order_album_name); +create index if not exists album_order_album_artist_name + on album (order_album_artist_name); + +alter table media_file + add order_album_name varchar(255) collate nocase; +alter table media_file + add order_album_artist_name varchar(255) collate nocase; +alter table media_file + add order_artist_name varchar(255) collate nocase; +alter table media_file + add sort_album_name varchar(255) collate nocase; +alter table media_file + add sort_artist_name varchar(255) collate nocase; +alter table media_file + add sort_album_artist_name varchar(255) collate nocase; +alter table media_file + add sort_title varchar(255) collate nocase; +create index if not exists media_file_order_album_name + on media_file (order_album_name); +create index if not exists media_file_order_artist_name + on media_file (order_artist_name); +`) + if err != nil { + return err + } + notice(tx, "A full rescan will be performed to change the search behaviour") + return forceFullRescan(tx) +} + +func Down20200423204116(tx *sql.Tx) error { + return nil +} diff --git a/model/album.go b/model/album.go index 69820cc4..3028bf02 100644 --- a/model/album.go +++ b/model/album.go @@ -3,23 +3,28 @@ package model import "time" type Album struct { - ID string `json:"id" orm:"column(id)"` - Name string `json:"name"` - CoverArtPath string `json:"coverArtPath"` - CoverArtId string `json:"coverArtId"` - ArtistID string `json:"artistId" orm:"pk;column(artist_id)"` - Artist string `json:"artist"` - AlbumArtistID string `json:"albumArtistId" orm:"pk;column(album_artist_id)"` - AlbumArtist string `json:"albumArtist"` - MaxYear int `json:"maxYear"` - MinYear int `json:"minYear"` - Compilation bool `json:"compilation"` - SongCount int `json:"songCount"` - Duration float32 `json:"duration"` - Genre string `json:"genre"` - FullText string `json:"fullText"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` + ID string `json:"id" orm:"column(id)"` + Name string `json:"name"` + CoverArtPath string `json:"coverArtPath"` + CoverArtId string `json:"coverArtId"` + ArtistID string `json:"artistId" orm:"pk;column(artist_id)"` + Artist string `json:"artist"` + AlbumArtistID string `json:"albumArtistId" orm:"pk;column(album_artist_id)"` + AlbumArtist string `json:"albumArtist"` + MaxYear int `json:"maxYear"` + MinYear int `json:"minYear"` + Compilation bool `json:"compilation"` + SongCount int `json:"songCount"` + Duration float32 `json:"duration"` + Genre string `json:"genre"` + FullText string `json:"fullText"` + SortAlbumName string `json:"sortAlbumName"` + SortArtistName string `json:"sortArtistName"` + SortAlbumArtistName string `json:"sortAlbumArtistName"` + OrderAlbumName string `json:"orderAlbumName"` + OrderAlbumArtistName string `json:"orderAlbumArtistName"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` // Annotations PlayCount int `json:"playCount" orm:"-"` diff --git a/model/artist.go b/model/artist.go index a771e12d..123e9c19 100644 --- a/model/artist.go +++ b/model/artist.go @@ -3,10 +3,12 @@ package model import "time" type Artist struct { - ID string `json:"id" orm:"column(id)"` - Name string `json:"name"` - AlbumCount int `json:"albumCount" orm:"column(album_count)"` - FullText string `json:"fullText"` + ID string `json:"id" orm:"column(id)"` + Name string `json:"name"` + AlbumCount int `json:"albumCount" orm:"column(album_count)"` + FullText string `json:"fullText"` + SortArtistName string `json:"sortArtistName"` + OrderArtistName string `json:"orderArtistName"` // Annotations PlayCount int `json:"playCount" orm:"-"` diff --git a/model/mediafile.go b/model/mediafile.go index 14115a49..bb1772ec 100644 --- a/model/mediafile.go +++ b/model/mediafile.go @@ -6,28 +6,34 @@ import ( ) type MediaFile struct { - ID string `json:"id" orm:"pk;column(id)"` - Path string `json:"path"` - Title string `json:"title"` - Album string `json:"album"` - ArtistID string `json:"artistId" orm:"pk;column(artist_id)"` - Artist string `json:"artist"` - AlbumArtistID string `json:"albumArtistId"` - AlbumArtist string `json:"albumArtist"` - AlbumID string `json:"albumId" orm:"pk;column(album_id)"` - HasCoverArt bool `json:"hasCoverArt"` - TrackNumber int `json:"trackNumber"` - DiscNumber int `json:"discNumber"` - Year int `json:"year"` - Size int `json:"size"` - Suffix string `json:"suffix"` - Duration float32 `json:"duration"` - BitRate int `json:"bitRate"` - Genre string `json:"genre"` - FullText string `json:"fullText"` - Compilation bool `json:"compilation"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` + ID string `json:"id" orm:"pk;column(id)"` + Path string `json:"path"` + Title string `json:"title"` + Album string `json:"album"` + ArtistID string `json:"artistId" orm:"pk;column(artist_id)"` + Artist string `json:"artist"` + AlbumArtistID string `json:"albumArtistId"` + AlbumArtist string `json:"albumArtist"` + AlbumID string `json:"albumId" orm:"pk;column(album_id)"` + HasCoverArt bool `json:"hasCoverArt"` + TrackNumber int `json:"trackNumber"` + DiscNumber int `json:"discNumber"` + Year int `json:"year"` + Size int `json:"size"` + Suffix string `json:"suffix"` + Duration float32 `json:"duration"` + BitRate int `json:"bitRate"` + Genre string `json:"genre"` + FullText string `json:"fullText"` + SortTitle string `json:"sortTitle"` + SortAlbumName string `json:"sortAlbumName"` + SortArtistName string `json:"sortArtistName"` + SortAlbumArtistName string `json:"sortAlbumArtistName"` + OrderAlbumName string `json:"orderAlbumName"` + OrderAlbumArtistName string `json:"orderAlbumArtistName"` + Compilation bool `json:"compilation"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` // Annotations PlayCount int `json:"playCount" orm:"-"` diff --git a/persistence/album_repository.go b/persistence/album_repository.go index 0d62843d..c0cadc5e 100644 --- a/persistence/album_repository.go +++ b/persistence/album_repository.go @@ -115,6 +115,8 @@ func (r *albumRepository) Refresh(ids ...string) error { } var albums []refreshAlbum sel := Select(`album_id as id, album as name, f.artist, f.album_artist, f.artist_id, f.album_artist_id, + f.sort_album_name, f.sort_artist_name, f.sort_album_artist_name, + f.order_album_name, f.order_album_artist_name, f.compilation, f.genre, max(f.year) as max_year, sum(f.duration) as duration, count(*) as song_count, a.id as current_id, f.id as cover_art_id, f.path as cover_art_path, f.has_cover_art, group_concat(f.artist, ' ') as song_artists, group_concat(f.year, ' ') as years`). @@ -148,7 +150,8 @@ func (r *albumRepository) Refresh(ids ...string) error { toInsert++ al.CreatedAt = time.Now() } - al.FullText = getFullText(al.Name, al.Artist, al.AlbumArtist, al.SongArtists) + al.FullText = getFullText(al.Name, al.Artist, al.AlbumArtist, al.SongArtists, + al.SortAlbumName, al.SortArtistName, al.SortAlbumArtistName) _, err := r.put(al.ID, al.Album) if err != nil { return err diff --git a/persistence/artist_repository.go b/persistence/artist_repository.go index 7b161b24..12f0aa28 100644 --- a/persistence/artist_repository.go +++ b/persistence/artist_repository.go @@ -56,7 +56,7 @@ func (r *artistRepository) getIndexKey(a *model.Artist) string { } func (r *artistRepository) Put(a *model.Artist) error { - a.FullText = getFullText(a.Name) + a.FullText = getFullText(a.Name, a.SortArtistName) _, err := r.put(a.ID, a) return err } @@ -111,7 +111,9 @@ func (r *artistRepository) Refresh(ids ...string) error { CurrentId string } var artists []refreshArtist - sel := Select("f.album_artist_id as id", "f.album_artist as name", "count(*) as album_count", "a.id as current_id"). + sel := Select("f.album_artist_id as id", "f.album_artist as name", "count(*) as album_count", "a.id as current_id", + "f.sort_album_artist_name as sort_artist_name", + "f.order_album_artist_name as order_artist_name"). From("album f"). LeftJoin("artist a on f.album_artist_id = a.id"). Where(Eq{"f.album_artist_id": ids}). diff --git a/persistence/mediafile_repository.go b/persistence/mediafile_repository.go index a725e2f6..3c893b9f 100644 --- a/persistence/mediafile_repository.go +++ b/persistence/mediafile_repository.go @@ -41,7 +41,8 @@ func (r mediaFileRepository) Exists(id string) (bool, error) { } func (r mediaFileRepository) Put(m *model.MediaFile) error { - m.FullText = getFullText(m.Title, m.Album, m.Artist, m.AlbumArtist) + m.FullText = getFullText(m.Title, m.Album, m.Artist, m.AlbumArtist, + m.SortTitle, m.SortAlbumName, m.SortArtistName, m.SortAlbumArtistName) _, err := r.put(m.ID, m) return err } diff --git a/scanner/metadata_ffmpeg.go b/scanner/metadata_ffmpeg.go index 9ca90a6d..cf154507 100644 --- a/scanner/metadata_ffmpeg.go +++ b/scanner/metadata_ffmpeg.go @@ -3,6 +3,7 @@ package scanner import ( "bufio" "errors" + "fmt" "mime" "os" "os/exec" @@ -27,7 +28,11 @@ type Metadata struct { func (m *Metadata) Title() string { return m.getTag("title", "sort_name") } func (m *Metadata) Album() string { return m.getTag("album", "sort_album") } func (m *Metadata) Artist() string { return m.getTag("artist", "sort_artist") } -func (m *Metadata) AlbumArtist() string { return m.getTag("album_artist") } +func (m *Metadata) AlbumArtist() string { return m.getTag("album_artist", "albumartist") } +func (m *Metadata) SortTitle() string { return m.getSortTag("title", "name") } +func (m *Metadata) SortAlbum() string { return m.getSortTag("album") } +func (m *Metadata) SortArtist() string { return m.getSortTag("artist") } +func (m *Metadata) SortAlbumArtist() string { return m.getSortTag("albumartist", "album_artist") } func (m *Metadata) Composer() string { return m.getTag("composer", "tcm", "sort_composer") } func (m *Metadata) Genre() string { return m.getTag("genre") } func (m *Metadata) Year() int { return m.parseYear("date") } @@ -99,7 +104,7 @@ var ( inputRegex = regexp.MustCompile(`(?m)^Input #\d+,.*,\sfrom\s'(.*)'`) // TITLE : Back In Black - tagsRx = regexp.MustCompile(`(?i)^\s{4,6}(\w+)\s*:(.*)`) + tagsRx = regexp.MustCompile(`(?i)^\s{4,6}([\w-]+)\s*:(.*)`) // Duration: 00:04:16.00, start: 0.000000, bitrate: 995 kb/s` durationRx = regexp.MustCompile(`^\s\sDuration: ([\d.:]+).*bitrate: (\d+)`) @@ -230,6 +235,18 @@ func (m *Metadata) getTag(tags ...string) string { return "" } +func (m *Metadata) getSortTag(tags ...string) string { + formats := []string{"sort%s", "sort_%s", "sort-%s", "%ssort", "%s_sort", "%s-sort"} + var all []string + for _, tag := range tags { + for _, format := range formats { + name := fmt.Sprintf(format, tag) + all = append(all, name) + } + } + return m.getTag(all...) +} + func (m *Metadata) parseTuple(tags ...string) (int, int) { for _, tagName := range tags { if v, ok := m.tags[tagName]; ok { diff --git a/scanner/metadata_test.go b/scanner/metadata_test.go index 649fc02a..cd7b2e73 100644 --- a/scanner/metadata_test.go +++ b/scanner/metadata_test.go @@ -204,6 +204,30 @@ Tracklist: md, _ := extractMetadata("tests/fixtures/test.mp3", outputWithMultilineComment) Expect(md.Comment()).To(Equal(expectedComment)) }) + + It("parses sort tags correctly", func() { + const output = ` +Input #0, mp3, from '/Users/deluan/Downloads/椎名林檎 - 加爾基 精液 栗ノ花 - 2003/02 - ドツペルゲンガー.mp3': + Metadata: + title-sort : Dopperugengā + album : 加爾基 精液 栗ノ花 + artist : 椎名林檎 + album_artist : 椎名林檎 + title : ドツペルゲンガー + albumsort : Kalk Samen Kuri No Hana + artist_sort : Shiina, Ringo + ALBUMARTISTSORT : Shiina, Ringo +` + md, _ := extractMetadata("tests/fixtures/test.mp3", output) + Expect(md.Title()).To(Equal("ドツペルゲンガー")) + Expect(md.Album()).To(Equal("加爾基 精液 栗ノ花")) + Expect(md.Artist()).To(Equal("椎名林檎")) + Expect(md.AlbumArtist()).To(Equal("椎名林檎")) + Expect(md.SortTitle()).To(Equal("Dopperugengā")) + Expect(md.SortAlbum()).To(Equal("Kalk Samen Kuri No Hana")) + Expect(md.SortArtist()).To(Equal("Shiina, Ringo")) + Expect(md.SortAlbumArtist()).To(Equal("Shiina, Ringo")) + }) }) Context("parseYear", func() { diff --git a/scanner/tag_scanner.go b/scanner/tag_scanner.go index db3f5ee4..6402cc90 100644 --- a/scanner/tag_scanner.go +++ b/scanner/tag_scanner.go @@ -262,6 +262,10 @@ func (s *TagScanner) toMediaFile(md *Metadata) model.MediaFile { mf.Suffix = md.Suffix() mf.Size = md.Size() mf.HasCoverArt = md.HasPicture() + mf.SortTitle = md.SortTitle() + mf.SortAlbumName = md.SortAlbum() + mf.SortArtistName = md.SortArtist() + mf.SortAlbumArtistName = md.SortAlbumArtist() // TODO Get Creation time. https://github.com/djherbis/times ? mf.CreatedAt = md.ModificationTime()