/* Copyright © 2023 Philipp Wolfer This file is part of Scotty. Scotty 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 3 of the License, or (at your option) any later version. Scotty 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 Scotty. If not, see . */ package jspf import ( "os" "time" "go.uploadedlobster.com/scotty/internal/config" "go.uploadedlobster.com/scotty/internal/i18n" "go.uploadedlobster.com/scotty/internal/models" "go.uploadedlobster.com/scotty/pkg/jspf" ) type JSPFBackend struct { filePath string playlist jspf.Playlist append bool } func (b *JSPFBackend) Name() string { return "jspf" } func (b *JSPFBackend) Options() []models.BackendOption { return []models.BackendOption{{ Name: "file-path", Label: i18n.Tr("File path"), Type: models.String, }, { Name: "append", Label: i18n.Tr("Append to file"), Type: models.Bool, Default: "true", }, { Name: "title", Label: i18n.Tr("Playlist title"), Type: models.String, }, { Name: "username", Label: i18n.Tr("User name"), Type: models.String, }, { Name: "identifier", Label: i18n.Tr("Unique playlist identifier"), Type: models.String, }} } func (b *JSPFBackend) FromConfig(config *config.ServiceConfig) models.Backend { b.filePath = config.GetString("file-path") b.append = true if config.IsSet("append") { b.append = config.GetBool("append") } b.playlist = jspf.Playlist{ Title: config.GetString("title"), Creator: config.GetString("username"), Identifier: config.GetString("identifier"), Tracks: make([]jspf.Track, 0), Extension: map[string]any{ jspf.MusicBrainzPlaylistExtensionId: jspf.MusicBrainzPlaylistExtension{ LastModifiedAt: time.Now(), Public: true, }, }, } return b } func (b *JSPFBackend) StartImport() error { return b.readJSPF() } func (b *JSPFBackend) FinishImport() error { return b.writeJSPF() } func (b *JSPFBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.Progress) (models.ImportResult, error) { for _, listen := range export.Items { track := listenAsTrack(listen) b.playlist.Tracks = append(b.playlist.Tracks, track) importResult.ImportCount += 1 importResult.UpdateTimestamp(listen.ListenedAt) } progress <- models.Progress{}.FromImportResult(importResult) return importResult, nil } func (b *JSPFBackend) ImportLoves(export models.LovesResult, importResult models.ImportResult, progress chan models.Progress) (models.ImportResult, error) { for _, love := range export.Items { track := loveAsTrack(love) b.playlist.Tracks = append(b.playlist.Tracks, track) importResult.ImportCount += 1 importResult.UpdateTimestamp(love.Created) } progress <- models.Progress{}.FromImportResult(importResult) return importResult, nil } func listenAsTrack(l models.Listen) jspf.Track { l.FillAdditionalInfo() track := trackAsTrack(l.Track) extension := makeMusicBrainzExtension(l.Track) extension.AddedAt = l.ListenedAt extension.AddedBy = l.UserName track.Extension[jspf.MusicBrainzTrackExtensionId] = extension if l.RecordingMbid != "" { track.Identifier = append(track.Identifier, "https://musicbrainz.org/recording/"+string(l.RecordingMbid)) } return track } func loveAsTrack(l models.Love) jspf.Track { l.FillAdditionalInfo() track := trackAsTrack(l.Track) extension := makeMusicBrainzExtension(l.Track) extension.AddedAt = l.Created extension.AddedBy = l.UserName track.Extension[jspf.MusicBrainzTrackExtensionId] = extension recordingMbid := l.Track.RecordingMbid if l.RecordingMbid != "" { recordingMbid = l.RecordingMbid } if recordingMbid != "" { track.Identifier = append(track.Identifier, "https://musicbrainz.org/recording/"+string(recordingMbid)) } return track } func trackAsTrack(t models.Track) jspf.Track { track := jspf.Track{ Title: t.TrackName, Album: t.ReleaseName, Creator: t.ArtistName(), TrackNum: t.TrackNumber, Extension: map[string]any{}, } return track } func makeMusicBrainzExtension(t models.Track) jspf.MusicBrainzTrackExtension { extension := jspf.MusicBrainzTrackExtension{ AdditionalMetadata: t.AdditionalInfo, ArtistIdentifiers: make([]string, len(t.ArtistMbids)), } for i, mbid := range t.ArtistMbids { extension.ArtistIdentifiers[i] = "https://musicbrainz.org/artist/" + string(mbid) } if t.ReleaseMbid != "" { extension.ReleaseIdentifier = "https://musicbrainz.org/release/" + string(t.ReleaseMbid) } // The tracknumber tag would be redundant delete(extension.AdditionalMetadata, "tracknumber") return extension } func (b *JSPFBackend) readJSPF() error { if b.append { file, err := os.Open(b.filePath) if err != nil { return err } defer file.Close() stat, err := file.Stat() if err != nil { return err } if stat.Size() == 0 { // Zero length file, treat as a new file return nil } else { playlist := jspf.JSPF{} err := playlist.Read(file) if err != nil { return err } b.playlist = playlist.Playlist } } return nil } func (b *JSPFBackend) writeJSPF() error { playlist := jspf.JSPF{ Playlist: b.playlist, } file, err := os.Create(b.filePath) if err != nil { return err } defer file.Close() return playlist.Write(file) }