From b3136bde9a7f7d22a891c544a5b14231cd2226c4 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 4 May 2025 11:43:26 +0200 Subject: [PATCH 01/14] jspf: add MB extension, if it does not exist --- internal/backends/jspf/jspf.go | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/internal/backends/jspf/jspf.go b/internal/backends/jspf/jspf.go index 6551f15..9d72765 100644 --- a/internal/backends/jspf/jspf.go +++ b/internal/backends/jspf/jspf.go @@ -77,14 +77,11 @@ func (b *JSPFBackend) InitConfig(config *config.ServiceConfig) error { Title: config.GetString("title"), Creator: config.GetString("username"), Identifier: config.GetString("identifier"), + Date: time.Now(), Tracks: make([]jspf.Track, 0), - Extension: jspf.ExtensionMap{ - jspf.MusicBrainzPlaylistExtensionID: jspf.MusicBrainzPlaylistExtension{ - LastModifiedAt: time.Now(), - Public: true, - }, - }, } + + b.addMusicBrainzPlaylistExtension() return nil } @@ -331,6 +328,7 @@ func (b *JSPFBackend) readJSPF() error { return err } b.playlist = playlist.Playlist + b.addMusicBrainzPlaylistExtension() } } @@ -350,3 +348,13 @@ func (b *JSPFBackend) writeJSPF() error { defer file.Close() return playlist.Write(file) } + +func (b *JSPFBackend) addMusicBrainzPlaylistExtension() { + if b.playlist.Extension == nil { + b.playlist.Extension = make(jspf.ExtensionMap, 1) + } + extension := jspf.MusicBrainzPlaylistExtension{Public: true} + b.playlist.Extension.Get(jspf.MusicBrainzPlaylistExtensionID, &extension) + extension.LastModifiedAt = time.Now() + b.playlist.Extension[jspf.MusicBrainzPlaylistExtensionID] = extension +} From 9480c69cbb42be234df13a7c6e52400370a3afe0 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Fri, 2 May 2025 08:36:59 +0200 Subject: [PATCH 02/14] Handle wait group for progress bar centrally This does not need to be exposed and caller only needs to wait for the Progress instance. --- internal/cli/progress.go | 5 +++-- internal/cli/transfer.go | 7 ++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/internal/cli/progress.go b/internal/cli/progress.go index 6d4421d..54ee4a8 100644 --- a/internal/cli/progress.go +++ b/internal/cli/progress.go @@ -1,5 +1,5 @@ /* -Copyright © 2023 Philipp Wolfer +Copyright © 2023-2025 Philipp Wolfer This file is part of Scotty. @@ -28,7 +28,8 @@ import ( "go.uploadedlobster.com/scotty/internal/models" ) -func progressBar(wg *sync.WaitGroup, exportProgress chan models.Progress, importProgress chan models.Progress) *mpb.Progress { +func progressBar(exportProgress chan models.Progress, importProgress chan models.Progress) *mpb.Progress { + wg := &sync.WaitGroup{} p := mpb.New( mpb.WithWaitGroup(wg), mpb.WithOutput(color.Output), diff --git a/internal/cli/transfer.go b/internal/cli/transfer.go index 0ba04b9..4777042 100644 --- a/internal/cli/transfer.go +++ b/internal/cli/transfer.go @@ -1,5 +1,5 @@ /* -Copyright © 2023 Philipp Wolfer +Copyright © 2023-2025 Philipp Wolfer 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 @@ -19,7 +19,6 @@ import ( "errors" "fmt" "strconv" - "sync" "time" "github.com/spf13/cobra" @@ -112,8 +111,7 @@ func (c *TransferCmd[E, I, R]) Transfer(exp backends.ExportProcessor[R], imp bac // Prepare progress bars exportProgress := make(chan models.Progress) importProgress := make(chan models.Progress) - var wg sync.WaitGroup - progress := progressBar(&wg, exportProgress, importProgress) + progress := progressBar(exportProgress, importProgress) // Export from source exportChan := make(chan R, 1000) @@ -126,7 +124,6 @@ func (c *TransferCmd[E, I, R]) Transfer(exp backends.ExportProcessor[R], imp bac if timestamp.After(result.LastTimestamp) { result.LastTimestamp = timestamp } - wg.Wait() progress.Wait() if result.Error != nil { printTimestamp("Import failed, last reported timestamp was %v (%s)", result.LastTimestamp) From 1c3364dad5b788b5ffa7c7b0451ccd2a469b17d8 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Fri, 2 May 2025 08:43:30 +0200 Subject: [PATCH 03/14] Close export results channel in generic implementation This removes the need for every implementation to handle this case. --- internal/backends/deezer/deezer.go | 4 ---- internal/backends/export.go | 8 +++++--- internal/backends/funkwhale/funkwhale.go | 4 ---- internal/backends/jspf/jspf.go | 4 ---- internal/backends/lastfm/lastfm.go | 4 ---- internal/backends/listenbrainz/listenbrainz.go | 3 --- internal/backends/maloja/maloja.go | 2 -- internal/backends/scrobblerlog/scrobblerlog.go | 1 - internal/backends/spotify/spotify.go | 4 ---- internal/backends/spotifyhistory/spotifyhistory.go | 2 -- internal/backends/subsonic/subsonic.go | 1 - 11 files changed, 5 insertions(+), 32 deletions(-) diff --git a/internal/backends/deezer/deezer.go b/internal/backends/deezer/deezer.go index 756e271..1a5cb30 100644 --- a/internal/backends/deezer/deezer.go +++ b/internal/backends/deezer/deezer.go @@ -88,8 +88,6 @@ func (b *DeezerApiBackend) ExportListens(oldestTimestamp time.Time, results chan totalDuration := startTime.Sub(oldestTimestamp) - defer close(results) - p := models.Progress{Total: int64(totalDuration.Seconds())} out: @@ -155,8 +153,6 @@ func (b *DeezerApiBackend) ExportLoves(oldestTimestamp time.Time, results chan m offset := math.MaxInt32 perPage := MaxItemsPerGet - defer close(results) - p := models.Progress{Total: int64(perPage)} var totalCount int diff --git a/internal/backends/export.go b/internal/backends/export.go index 44b8757..0346af2 100644 --- a/internal/backends/export.go +++ b/internal/backends/export.go @@ -1,5 +1,5 @@ /* -Copyright © 2023 Philipp Wolfer +Copyright © 2023-2025 Philipp Wolfer 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 @@ -35,8 +35,9 @@ func (p ListensExportProcessor) ExportBackend() models.Backend { } func (p ListensExportProcessor) Process(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.Progress) { + defer close(results) + defer close(progress) p.Backend.ExportListens(oldestTimestamp, results, progress) - close(progress) } type LovesExportProcessor struct { @@ -48,6 +49,7 @@ func (p LovesExportProcessor) ExportBackend() models.Backend { } func (p LovesExportProcessor) Process(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.Progress) { + defer close(results) + defer close(progress) p.Backend.ExportLoves(oldestTimestamp, results, progress) - close(progress) } diff --git a/internal/backends/funkwhale/funkwhale.go b/internal/backends/funkwhale/funkwhale.go index 3e296c1..3619869 100644 --- a/internal/backends/funkwhale/funkwhale.go +++ b/internal/backends/funkwhale/funkwhale.go @@ -64,8 +64,6 @@ func (b *FunkwhaleApiBackend) ExportListens(oldestTimestamp time.Time, results c page := 1 perPage := MaxItemsPerGet - defer close(results) - // We need to gather the full list of listens in order to sort them listens := make(models.ListensList, 0, 2*perPage) p := models.Progress{Total: int64(perPage)} @@ -113,8 +111,6 @@ func (b *FunkwhaleApiBackend) ExportLoves(oldestTimestamp time.Time, results cha page := 1 perPage := MaxItemsPerGet - defer close(results) - // We need to gather the full list of listens in order to sort them loves := make(models.LovesList, 0, 2*perPage) p := models.Progress{Total: int64(perPage)} diff --git a/internal/backends/jspf/jspf.go b/internal/backends/jspf/jspf.go index 9d72765..826ea1b 100644 --- a/internal/backends/jspf/jspf.go +++ b/internal/backends/jspf/jspf.go @@ -94,8 +94,6 @@ func (b *JSPFBackend) FinishImport() error { } func (b *JSPFBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.Progress) { - defer close(results) - err := b.readJSPF() if err != nil { progress <- models.Progress{}.Complete() @@ -128,8 +126,6 @@ func (b *JSPFBackend) ImportListens(export models.ListensResult, importResult mo } func (b *JSPFBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.Progress) { - defer close(results) - err := b.readJSPF() if err != nil { progress <- models.Progress{}.Complete() diff --git a/internal/backends/lastfm/lastfm.go b/internal/backends/lastfm/lastfm.go index 76fe9c7..444e5b0 100644 --- a/internal/backends/lastfm/lastfm.go +++ b/internal/backends/lastfm/lastfm.go @@ -93,8 +93,6 @@ func (b *LastfmApiBackend) ExportListens(oldestTimestamp time.Time, results chan minTime := oldestTimestamp perPage := MaxItemsPerGet - defer close(results) - // We need to gather the full list of listens in order to sort them p := models.Progress{Total: int64(page)} @@ -258,8 +256,6 @@ func (b *LastfmApiBackend) ExportLoves(oldestTimestamp time.Time, results chan m page := 1 perPage := MaxItemsPerGet - defer close(results) - loves := make(models.LovesList, 0, 2*MaxItemsPerGet) p := models.Progress{Total: int64(perPage)} var totalCount int diff --git a/internal/backends/listenbrainz/listenbrainz.go b/internal/backends/listenbrainz/listenbrainz.go index 6c7b747..e1ea53d 100644 --- a/internal/backends/listenbrainz/listenbrainz.go +++ b/internal/backends/listenbrainz/listenbrainz.go @@ -81,8 +81,6 @@ func (b *ListenBrainzApiBackend) ExportListens(oldestTimestamp time.Time, result totalDuration := startTime.Sub(minTime) - defer close(results) - p := models.Progress{Total: int64(totalDuration.Seconds())} for { @@ -195,7 +193,6 @@ func (b *ListenBrainzApiBackend) ImportListens(export models.ListensResult, impo } func (b *ListenBrainzApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.Progress) { - defer close(results) exportChan := make(chan models.LovesResult) p := models.Progress{} diff --git a/internal/backends/maloja/maloja.go b/internal/backends/maloja/maloja.go index e9e3348..6bcdcc2 100644 --- a/internal/backends/maloja/maloja.go +++ b/internal/backends/maloja/maloja.go @@ -67,8 +67,6 @@ func (b *MalojaApiBackend) ExportListens(oldestTimestamp time.Time, results chan page := 0 perPage := MaxItemsPerGet - defer close(results) - // We need to gather the full list of listens in order to sort them listens := make(models.ListensList, 0, 2*perPage) p := models.Progress{Total: int64(perPage)} diff --git a/internal/backends/scrobblerlog/scrobblerlog.go b/internal/backends/scrobblerlog/scrobblerlog.go index 14ee24f..a355e3e 100644 --- a/internal/backends/scrobblerlog/scrobblerlog.go +++ b/internal/backends/scrobblerlog/scrobblerlog.go @@ -132,7 +132,6 @@ func (b *ScrobblerLogBackend) FinishImport() error { } func (b *ScrobblerLogBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.Progress) { - defer close(results) file, err := os.Open(b.filePath) if err != nil { progress <- models.Progress{}.Complete() diff --git a/internal/backends/spotify/spotify.go b/internal/backends/spotify/spotify.go index 8c17903..8b6d9da 100644 --- a/internal/backends/spotify/spotify.go +++ b/internal/backends/spotify/spotify.go @@ -101,8 +101,6 @@ func (b *SpotifyApiBackend) ExportListens(oldestTimestamp time.Time, results cha totalDuration := startTime.Sub(oldestTimestamp) - defer close(results) - p := models.Progress{Total: int64(totalDuration.Seconds())} for { @@ -163,8 +161,6 @@ func (b *SpotifyApiBackend) ExportLoves(oldestTimestamp time.Time, results chan offset := math.MaxInt32 perPage := MaxItemsPerGet - defer close(results) - p := models.Progress{Total: int64(perPage)} totalCount := 0 exportCount := 0 diff --git a/internal/backends/spotifyhistory/spotifyhistory.go b/internal/backends/spotifyhistory/spotifyhistory.go index 1c986be..c150d3b 100644 --- a/internal/backends/spotifyhistory/spotifyhistory.go +++ b/internal/backends/spotifyhistory/spotifyhistory.go @@ -73,8 +73,6 @@ func (b *SpotifyHistoryBackend) InitConfig(config *config.ServiceConfig) error { } func (b *SpotifyHistoryBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.Progress) { - defer close(results) - files, err := filepath.Glob(path.Join(b.dirPath, historyFileGlob)) if err != nil { progress <- models.Progress{}.Complete() diff --git a/internal/backends/subsonic/subsonic.go b/internal/backends/subsonic/subsonic.go index d605324..a966c68 100644 --- a/internal/backends/subsonic/subsonic.go +++ b/internal/backends/subsonic/subsonic.go @@ -64,7 +64,6 @@ func (b *SubsonicApiBackend) InitConfig(config *config.ServiceConfig) error { } func (b *SubsonicApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.Progress) { - defer close(results) err := b.client.Authenticate(b.password) if err != nil { progress <- models.Progress{}.Complete() From 3b1adc9f1f81288efbea8174d8edc642c4173d9e Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Fri, 2 May 2025 09:25:07 +0200 Subject: [PATCH 04/14] Fix duplicate calls to handle import errors This fixes the import process hanging on error --- internal/backends/import.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/backends/import.go b/internal/backends/import.go index 6173a53..9938c10 100644 --- a/internal/backends/import.go +++ b/internal/backends/import.go @@ -1,5 +1,5 @@ /* -Copyright © 2023 Philipp Wolfer +Copyright © 2023-2025 Philipp Wolfer This file is part of Scotty. @@ -41,7 +41,7 @@ func (p ListensImportProcessor) Process(results chan models.ListensResult, out c func (p ListensImportProcessor) Import(export models.ListensResult, result models.ImportResult, out chan models.ImportResult, progress chan models.Progress) (models.ImportResult, error) { if export.Error != nil { - return handleError(result, export.Error, progress), export.Error + return result, export.Error } if export.Total > 0 { @@ -51,7 +51,7 @@ func (p ListensImportProcessor) Import(export models.ListensResult, result model } importResult, err := p.Backend.ImportListens(export, result, progress) if err != nil { - return handleError(result, err, progress), err + return result, err } return importResult, nil } @@ -70,7 +70,7 @@ func (p LovesImportProcessor) Process(results chan models.LovesResult, out chan func (p LovesImportProcessor) Import(export models.LovesResult, result models.ImportResult, out chan models.ImportResult, progress chan models.Progress) (models.ImportResult, error) { if export.Error != nil { - return handleError(result, export.Error, progress), export.Error + return result, export.Error } if export.Total > 0 { @@ -80,7 +80,7 @@ func (p LovesImportProcessor) Import(export models.LovesResult, result models.Im } importResult, err := p.Backend.ImportLoves(export, result, progress) if err != nil { - return handleError(importResult, err, progress), err + return result, err } return importResult, nil } From 069f0de2ee9db6cdf12a36c09ff5227e5ab18000 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 4 May 2025 10:31:36 +0200 Subject: [PATCH 05/14] Call "FinishImport" even on error This gives the importer the chance to close connections and free resources to ensure already imported items are properly handled. --- internal/backends/import.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/internal/backends/import.go b/internal/backends/import.go index 9938c10..0db5547 100644 --- a/internal/backends/import.go +++ b/internal/backends/import.go @@ -90,8 +90,7 @@ func process[R models.LovesResult | models.ListensResult, P ImportProcessor[R]]( defer close(progress) result := models.ImportResult{} - err := processor.ImportBackend().StartImport() - if err != nil { + if err := processor.ImportBackend().StartImport(); err != nil { out <- handleError(result, err, progress) return } @@ -99,6 +98,7 @@ func process[R models.LovesResult | models.ListensResult, P ImportProcessor[R]]( for exportResult := range results { importResult, err := processor.Import(exportResult, result, out, progress) if err != nil { + processor.ImportBackend().FinishImport() out <- handleError(result, err, progress) return } @@ -106,8 +106,7 @@ func process[R models.LovesResult | models.ListensResult, P ImportProcessor[R]]( progress <- models.Progress{}.FromImportResult(result) } - err = processor.ImportBackend().FinishImport() - if err != nil { + if err := processor.ImportBackend().FinishImport(); err != nil { out <- handleError(result, err, progress) return } From 55ac41b147a763651a4e5690ec4282800a6eff85 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 4 May 2025 11:52:12 +0200 Subject: [PATCH 06/14] If import fails still save the last reported timestamp This allows continuing a partially failed import run. --- internal/backends/import.go | 11 ++++++----- internal/backends/maloja/maloja.go | 3 ++- internal/cli/transfer.go | 16 +++++++--------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/internal/backends/import.go b/internal/backends/import.go index 0db5547..c0d78bc 100644 --- a/internal/backends/import.go +++ b/internal/backends/import.go @@ -51,7 +51,7 @@ func (p ListensImportProcessor) Import(export models.ListensResult, result model } importResult, err := p.Backend.ImportListens(export, result, progress) if err != nil { - return result, err + return importResult, err } return importResult, nil } @@ -80,7 +80,7 @@ func (p LovesImportProcessor) Import(export models.LovesResult, result models.Im } importResult, err := p.Backend.ImportLoves(export, result, progress) if err != nil { - return result, err + return importResult, err } return importResult, nil } @@ -89,6 +89,7 @@ func process[R models.LovesResult | models.ListensResult, P ImportProcessor[R]]( defer close(out) defer close(progress) result := models.ImportResult{} + p := models.Progress{} if err := processor.ImportBackend().StartImport(); err != nil { out <- handleError(result, err, progress) @@ -97,13 +98,13 @@ func process[R models.LovesResult | models.ListensResult, P ImportProcessor[R]]( for exportResult := range results { importResult, err := processor.Import(exportResult, result, out, progress) + result.Update(importResult) if err != nil { processor.ImportBackend().FinishImport() out <- handleError(result, err, progress) return } - result.Update(importResult) - progress <- models.Progress{}.FromImportResult(result) + progress <- p.FromImportResult(result) } if err := processor.ImportBackend().FinishImport(); err != nil { @@ -111,7 +112,7 @@ func process[R models.LovesResult | models.ListensResult, P ImportProcessor[R]]( return } - progress <- models.Progress{}.FromImportResult(result).Complete() + progress <- p.FromImportResult(result).Complete() out <- result } diff --git a/internal/backends/maloja/maloja.go b/internal/backends/maloja/maloja.go index 6bcdcc2..a22393b 100644 --- a/internal/backends/maloja/maloja.go +++ b/internal/backends/maloja/maloja.go @@ -105,6 +105,7 @@ out: } func (b *MalojaApiBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.Progress) (models.ImportResult, error) { + p := models.Progress{}.FromImportResult(importResult) for _, listen := range export.Items { scrobble := NewScrobble{ Title: listen.TrackName, @@ -125,7 +126,7 @@ func (b *MalojaApiBackend) ImportListens(export models.ListensResult, importResu importResult.UpdateTimestamp(listen.ListenedAt) importResult.ImportCount += 1 - progress <- models.Progress{}.FromImportResult(importResult) + progress <- p.FromImportResult(importResult) } return importResult, nil diff --git a/internal/cli/transfer.go b/internal/cli/transfer.go index 4777042..83bad2a 100644 --- a/internal/cli/transfer.go +++ b/internal/cli/transfer.go @@ -121,16 +121,7 @@ func (c *TransferCmd[E, I, R]) Transfer(exp backends.ExportProcessor[R], imp bac resultChan := make(chan models.ImportResult) go imp.Process(exportChan, resultChan, importProgress) result := <-resultChan - if timestamp.After(result.LastTimestamp) { - result.LastTimestamp = timestamp - } progress.Wait() - if result.Error != nil { - printTimestamp("Import failed, last reported timestamp was %v (%s)", result.LastTimestamp) - return result.Error - } - fmt.Println(i18n.Tr("Imported %v of %v %s into %v.", - result.ImportCount, result.TotalCount, c.entity, c.targetName)) // Update timestamp err = c.updateTimestamp(result, timestamp) @@ -138,6 +129,13 @@ func (c *TransferCmd[E, I, R]) Transfer(exp backends.ExportProcessor[R], imp bac return err } + fmt.Println(i18n.Tr("Imported %v of %v %s into %v.", + result.ImportCount, result.TotalCount, c.entity, c.targetName)) + if result.Error != nil { + printTimestamp("Import failed, last reported timestamp was %v (%s)", result.LastTimestamp) + return result.Error + } + // Print errors if len(result.ImportLog) > 0 { fmt.Println() From 15d939e15098895c690bb4c055d84d23e800ceaa Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 4 May 2025 12:02:17 +0200 Subject: [PATCH 07/14] Update changelog --- CHANGES.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 5dfd892..ba2685b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,15 @@ # Scotty Changelog ## 0.6.0 - WIP -- JSPF: Implemented export as loves and listens +- Fix program hanging endlessly if import fails (#11) +- If import fails still store the last successfully imported timestamp +- JSPF: implemented export as loves and listens +- JSPF: write track duration +- JSPF: read username and recording MSID +- JSPF: add MusicBrainz playlist extension in append mode, if it does not exist + in the existing JSPF file +- scrobblerlog: fix timezone not being set from config (#6) +- scrobblerlog: fix listen export not considering latest timestamp ## 0.5.2 - 2025-05-01 @@ -20,9 +28,9 @@ - ListenBrainz: log missing recording MBID on love import - Subsonic: support OpenSubsonic fields for recording MBID and genres (#5) - Subsonic: fixed progress for loves export -- scrobblerlog: add "time-zone" config option (#6). +- scrobblerlog: add "time-zone" config option (#6) - scrobblerlog: fixed progress for listen export -- scrobblerlog: renamed setting `include-skipped` to `ignore-skipped`. +- scrobblerlog: renamed setting `include-skipped` to `ignore-skipped` Note: 386 builds for Linux are not available with this release due to an incompatibility with latest version of gorm. From aae5123c3d1ab54de3b784d998ea11bd144039d6 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 4 May 2025 12:59:40 +0200 Subject: [PATCH 08/14] Show progress bars as aborted on export / import error --- CHANGES.md | 1 + internal/backends/deezer/deezer.go | 4 ++-- internal/backends/import.go | 2 +- internal/backends/jspf/jspf.go | 4 ++-- internal/backends/lastfm/lastfm.go | 8 ++++---- internal/backends/listenbrainz/listenbrainz.go | 5 +++-- internal/backends/scrobblerlog/scrobblerlog.go | 2 +- internal/backends/spotify/spotify.go | 6 +++--- internal/backends/spotifyhistory/spotifyhistory.go | 4 ++-- internal/backends/subsonic/subsonic.go | 4 ++-- internal/cli/progress.go | 10 ++++++++-- internal/models/models.go | 6 ++++++ 12 files changed, 35 insertions(+), 21 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ba2685b..a0a60f2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,7 @@ ## 0.6.0 - WIP - Fix program hanging endlessly if import fails (#11) - If import fails still store the last successfully imported timestamp +- Show progress bars as aborted on export / import error - JSPF: implemented export as loves and listens - JSPF: write track duration - JSPF: read username and recording MSID diff --git a/internal/backends/deezer/deezer.go b/internal/backends/deezer/deezer.go index 1a5cb30..4ba367d 100644 --- a/internal/backends/deezer/deezer.go +++ b/internal/backends/deezer/deezer.go @@ -94,7 +94,7 @@ out: for { result, err := b.client.UserHistory(offset, perPage) if err != nil { - progress <- p.Complete() + progress <- p.Abort() results <- models.ListensResult{Error: err} return } @@ -160,7 +160,7 @@ out: for { result, err := b.client.UserTracks(offset, perPage) if err != nil { - progress <- p.Complete() + progress <- p.Abort() results <- models.LovesResult{Error: err} return } diff --git a/internal/backends/import.go b/internal/backends/import.go index c0d78bc..407082c 100644 --- a/internal/backends/import.go +++ b/internal/backends/import.go @@ -118,6 +118,6 @@ func process[R models.LovesResult | models.ListensResult, P ImportProcessor[R]]( func handleError(result models.ImportResult, err error, progress chan models.Progress) models.ImportResult { result.Error = err - progress <- models.Progress{}.FromImportResult(result).Complete() + progress <- models.Progress{}.FromImportResult(result).Abort() return result } diff --git a/internal/backends/jspf/jspf.go b/internal/backends/jspf/jspf.go index 826ea1b..e981741 100644 --- a/internal/backends/jspf/jspf.go +++ b/internal/backends/jspf/jspf.go @@ -96,7 +96,7 @@ func (b *JSPFBackend) FinishImport() error { func (b *JSPFBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.Progress) { err := b.readJSPF() if err != nil { - progress <- models.Progress{}.Complete() + progress <- models.Progress{}.Abort() results <- models.ListensResult{Error: err} return } @@ -128,7 +128,7 @@ func (b *JSPFBackend) ImportListens(export models.ListensResult, importResult mo func (b *JSPFBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.Progress) { err := b.readJSPF() if err != nil { - progress <- models.Progress{}.Complete() + progress <- models.Progress{}.Abort() results <- models.LovesResult{Error: err} return } diff --git a/internal/backends/lastfm/lastfm.go b/internal/backends/lastfm/lastfm.go index 444e5b0..d45f793 100644 --- a/internal/backends/lastfm/lastfm.go +++ b/internal/backends/lastfm/lastfm.go @@ -108,7 +108,7 @@ out: result, err := b.client.User.GetRecentTracks(args) if err != nil { results <- models.ListensResult{Error: err} - progress <- p.Complete() + progress <- p.Abort() return } @@ -127,7 +127,7 @@ out: timestamp, err := strconv.ParseInt(scrobble.Date.Uts, 10, 64) if err != nil { results <- models.ListensResult{Error: err} - progress <- p.Complete() + progress <- p.Abort() break out } if timestamp > oldestTimestamp.Unix() { @@ -268,7 +268,7 @@ out: "page": page, }) if err != nil { - progress <- p.Complete() + progress <- p.Abort() results <- models.LovesResult{Error: err} return } @@ -282,7 +282,7 @@ out: for _, track := range result.Tracks { timestamp, err := strconv.ParseInt(track.Date.Uts, 10, 64) if err != nil { - progress <- p.Complete() + progress <- p.Abort() results <- models.LovesResult{Error: err} return } diff --git a/internal/backends/listenbrainz/listenbrainz.go b/internal/backends/listenbrainz/listenbrainz.go index e1ea53d..fffe0f0 100644 --- a/internal/backends/listenbrainz/listenbrainz.go +++ b/internal/backends/listenbrainz/listenbrainz.go @@ -86,7 +86,7 @@ func (b *ListenBrainzApiBackend) ExportListens(oldestTimestamp time.Time, result for { result, err := b.client.GetListens(b.username, time.Now(), minTime) if err != nil { - progress <- p.Complete() + progress <- p.Abort() results <- models.ListensResult{Error: err} return } @@ -199,8 +199,9 @@ func (b *ListenBrainzApiBackend) ExportLoves(oldestTimestamp time.Time, results go b.exportLoves(oldestTimestamp, exportChan) for existingLoves := range exportChan { if existingLoves.Error != nil { - progress <- p.Complete() + progress <- p.Abort() results <- models.LovesResult{Error: existingLoves.Error} + return } p.Total = int64(existingLoves.Total) diff --git a/internal/backends/scrobblerlog/scrobblerlog.go b/internal/backends/scrobblerlog/scrobblerlog.go index a355e3e..7890971 100644 --- a/internal/backends/scrobblerlog/scrobblerlog.go +++ b/internal/backends/scrobblerlog/scrobblerlog.go @@ -134,7 +134,7 @@ func (b *ScrobblerLogBackend) FinishImport() error { func (b *ScrobblerLogBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.Progress) { file, err := os.Open(b.filePath) if err != nil { - progress <- models.Progress{}.Complete() + progress <- models.Progress{}.Abort() results <- models.ListensResult{Error: err} return } diff --git a/internal/backends/spotify/spotify.go b/internal/backends/spotify/spotify.go index 8b6d9da..be48dfe 100644 --- a/internal/backends/spotify/spotify.go +++ b/internal/backends/spotify/spotify.go @@ -106,7 +106,7 @@ func (b *SpotifyApiBackend) ExportListens(oldestTimestamp time.Time, results cha for { result, err := b.client.RecentlyPlayedAfter(minTime, MaxItemsPerGet) if err != nil { - progress <- p.Complete() + progress <- p.Abort() results <- models.ListensResult{Error: err} return } @@ -118,7 +118,7 @@ func (b *SpotifyApiBackend) ExportListens(oldestTimestamp time.Time, results cha // Set minTime to the newest returned listen after, err := strconv.ParseInt(result.Cursors.After, 10, 64) if err != nil { - progress <- p.Complete() + progress <- p.Abort() results <- models.ListensResult{Error: err} return } else if after <= minTime.Unix() { @@ -169,7 +169,7 @@ out: for { result, err := b.client.UserTracks(offset, perPage) if err != nil { - progress <- p.Complete() + progress <- p.Abort() results <- models.LovesResult{Error: err} return } diff --git a/internal/backends/spotifyhistory/spotifyhistory.go b/internal/backends/spotifyhistory/spotifyhistory.go index c150d3b..23309f3 100644 --- a/internal/backends/spotifyhistory/spotifyhistory.go +++ b/internal/backends/spotifyhistory/spotifyhistory.go @@ -75,7 +75,7 @@ func (b *SpotifyHistoryBackend) InitConfig(config *config.ServiceConfig) error { func (b *SpotifyHistoryBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.Progress) { files, err := filepath.Glob(path.Join(b.dirPath, historyFileGlob)) if err != nil { - progress <- models.Progress{}.Complete() + progress <- models.Progress{}.Abort() results <- models.ListensResult{Error: err} return } @@ -86,7 +86,7 @@ func (b *SpotifyHistoryBackend) ExportListens(oldestTimestamp time.Time, results for i, filePath := range files { history, err := readHistoryFile(filePath) if err != nil { - progress <- models.Progress{}.Complete() + progress <- models.Progress{}.Abort() results <- models.ListensResult{Error: err} return } diff --git a/internal/backends/subsonic/subsonic.go b/internal/backends/subsonic/subsonic.go index a966c68..370765e 100644 --- a/internal/backends/subsonic/subsonic.go +++ b/internal/backends/subsonic/subsonic.go @@ -66,14 +66,14 @@ func (b *SubsonicApiBackend) InitConfig(config *config.ServiceConfig) error { func (b *SubsonicApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.Progress) { err := b.client.Authenticate(b.password) if err != nil { - progress <- models.Progress{}.Complete() + progress <- models.Progress{}.Abort() results <- models.LovesResult{Error: err} return } starred, err := b.client.GetStarred2(map[string]string{}) if err != nil { - progress <- models.Progress{}.Complete() + progress <- models.Progress{}.Abort() results <- models.LovesResult{Error: err} return } diff --git a/internal/cli/progress.go b/internal/cli/progress.go index 54ee4a8..6b60697 100644 --- a/internal/cli/progress.go +++ b/internal/cli/progress.go @@ -59,10 +59,12 @@ func setupProgressBar(p *mpb.Progress, name string) *mpb.Bar { ), mpb.AppendDecorators( decor.OnComplete( - decor.EwmaETA(decor.ET_STYLE_GO, 0, decor.WC{C: decor.DSyncWidth}), + decor.OnAbort( + decor.EwmaETA(decor.ET_STYLE_GO, 0, decor.WC{C: decor.DSyncWidth}), + i18n.Tr("error"), + ), i18n.Tr("done"), ), - // decor.OnComplete(decor.Percentage(decor.WC{W: 5, C: decor.DSyncWidthR}), "done"), decor.Name(" "), ), ) @@ -73,6 +75,10 @@ func updateProgressBar(bar *mpb.Bar, wg *sync.WaitGroup, progressChan chan model defer wg.Done() lastIterTime := time.Now() for progress := range progressChan { + if progress.Aborted { + bar.Abort(false) + return + } oldIterTime := lastIterTime lastIterTime = time.Now() bar.EwmaSetCurrent(progress.Elapsed, lastIterTime.Sub(oldIterTime)) diff --git a/internal/models/models.go b/internal/models/models.go index f2dd71d..b8f9121 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -213,6 +213,7 @@ type Progress struct { Total int64 Elapsed int64 Completed bool + Aborted bool } func (p Progress) FromImportResult(result ImportResult) Progress { @@ -226,3 +227,8 @@ func (p Progress) Complete() Progress { p.Completed = true return p } + +func (p Progress) Abort() Progress { + p.Aborted = true + return p +} From dfe6773744a010fc77ea92fbb9ec6ec42fc52bf0 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 4 May 2025 13:07:02 +0200 Subject: [PATCH 09/14] Update translations --- internal/translations/catalog.go | 38 ++++++------- .../translations/locales/de/out.gotext.json | 47 ++++++++-------- .../translations/locales/en/out.gotext.json | 53 +++++++++++-------- 3 files changed, 76 insertions(+), 62 deletions(-) diff --git a/internal/translations/catalog.go b/internal/translations/catalog.go index 3eb2f7e..fbe5eef 100644 --- a/internal/translations/catalog.go +++ b/internal/translations/catalog.go @@ -61,9 +61,9 @@ var messageKeyToIndex = map[string]int{ "Ignore listens in incognito mode": 30, "Ignore skipped listens": 27, "Ignored duplicate listen %v: \"%v\" by %v (%v)": 25, - "Import failed, last reported timestamp was %v (%s)": 45, + "Import failed, last reported timestamp was %v (%s)": 46, "Import log:": 47, - "Imported %v of %v %s into %v.": 46, + "Imported %v of %v %s into %v.": 45, "Latest timestamp: %v (%v)": 50, "Minimum playback duration for skipped tracks (seconds)": 31, "No": 39, @@ -85,6 +85,7 @@ var messageKeyToIndex = map[string]int{ "a service with this name already exists": 4, "backend %s does not implement %s": 13, "done": 37, + "error": 54, "exporting": 35, "importing": 36, "invalid timestamp string \"%v\"": 49, @@ -95,7 +96,7 @@ var messageKeyToIndex = map[string]int{ "unknown backend \"%s\"": 14, } -var deIndex = []uint32{ // 55 elements +var deIndex = []uint32{ // 56 elements // Entry 0 - 1F 0x00000000, 0x00000013, 0x00000027, 0x00000052, 0x0000005e, 0x0000008d, 0x000000bd, 0x00000104, @@ -109,10 +110,10 @@ var deIndex = []uint32{ // 55 elements 0x00000418, 0x00000443, 0x0000046d, 0x000004ad, 0x000004b8, 0x000004c3, 0x000004ca, 0x000004cd, 0x000004d2, 0x000004fb, 0x00000503, 0x0000050b, - 0x00000534, 0x00000552, 0x0000058f, 0x000005ba, + 0x00000534, 0x00000552, 0x0000057d, 0x000005ba, 0x000005c5, 0x000005d2, 0x000005f6, 0x00000619, - 0x0000066a, 0x000006a1, 0x000006c8, -} // Size: 244 bytes + 0x0000066a, 0x000006a1, 0x000006c8, 0x000006c8, +} // Size: 248 bytes const deData string = "" + // Size: 1736 bytes "\x04\x01\x09\x00\x0e\x02Export: %[1]s\x04\x01\x09\x01\x0a\x0e\x02Import:" + @@ -136,15 +137,15 @@ const deData string = "" + // Size: 1736 bytes "rein\x04\x00\x01\x0a;\x02Zugriffstoken erhalten, %[1]v kann jetzt verwen" + "det werden.\x02exportiere\x02importiere\x02fertig\x02Ja\x02Nein\x02keine" + " bestehenden Servicekonfigurationen\x02Service\x02Backend\x02Übertrage %" + - "[1]s von %[2]s nach %[3]s…\x02Ab Zeitstempel: %[1]v (%[2]v)\x02Import fe" + - "hlgeschlagen, letzter Zeitstempel war %[1]v (%[2]s)\x02%[1]v von %[2]v %" + - "[3]s in %[4]v importiert.\x02Importlog:\x02%[1]v: %[2]v\x02ungültiger Ze" + + "[1]s von %[2]s nach %[3]s…\x02Ab Zeitstempel: %[1]v (%[2]v)\x02%[1]v von" + + " %[2]v %[3]s in %[4]v importiert.\x02Import fehlgeschlagen, letzter Zeit" + + "stempel war %[1]v (%[2]s)\x02Importlog:\x02%[1]v: %[2]v\x02ungültiger Ze" + "itstempel „%[1]v“\x02Letzter Zeitstempel: %[1]v (%[2]v)\x02keine Konfigu" + "rationsdatei definiert, Konfiguration kann nicht geschrieben werden\x02S" + "chlüssel darf nur die Zeichen A-Za-z0-9_- beinhalten\x02keine Servicekon" + "figuration „%[1]v“" -var enIndex = []uint32{ // 55 elements +var enIndex = []uint32{ // 56 elements // Entry 0 - 1F 0x00000000, 0x00000013, 0x00000027, 0x00000044, 0x00000051, 0x00000079, 0x000000a1, 0x000000de, @@ -158,12 +159,12 @@ var enIndex = []uint32{ // 55 elements 0x0000039c, 0x000003c3, 0x000003df, 0x00000412, 0x0000041c, 0x00000426, 0x0000042b, 0x0000042f, 0x00000432, 0x00000455, 0x0000045d, 0x00000465, - 0x0000048f, 0x000004ad, 0x000004e6, 0x00000510, + 0x0000048f, 0x000004ad, 0x000004d7, 0x00000510, 0x0000051c, 0x00000529, 0x0000054a, 0x0000056a, - 0x0000059d, 0x000005c2, 0x000005e3, -} // Size: 244 bytes + 0x0000059d, 0x000005c2, 0x000005e3, 0x000005e9, +} // Size: 248 bytes -const enData string = "" + // Size: 1507 bytes +const enData string = "" + // Size: 1513 bytes "\x04\x01\x09\x00\x0e\x02export: %[1]s\x04\x01\x09\x01\x0a\x0e\x02import:" + " %[1]s\x02Failed reading config: %[1]v\x02Service name\x02a service with" + " this name already exists\x02Saved service %[1]v using backend %[2]v\x02" + @@ -184,10 +185,11 @@ const enData string = "" + // Size: 1507 bytes "token received, you can use %[1]v now.\x02exporting\x02importing\x02done" + "\x02Yes\x02No\x02no existing service configurations\x02Service\x02Backen" + "d\x02Transferring %[1]s from %[2]s to %[3]s…\x02From timestamp: %[1]v (%" + - "[2]v)\x02Import failed, last reported timestamp was %[1]v (%[2]s)\x02Imp" + - "orted %[1]v of %[2]v %[3]s into %[4]v.\x02Import log:\x02%[1]v: %[2]v" + + "[2]v)\x02Imported %[1]v of %[2]v %[3]s into %[4]v.\x02Import failed, las" + + "t reported timestamp was %[1]v (%[2]s)\x02Import log:\x02%[1]v: %[2]v" + "\x02invalid timestamp string \x22%[1]v\x22\x02Latest timestamp: %[1]v (%" + "[2]v)\x02no configuration file defined, cannot write config\x02key must " + - "only consist of A-Za-z0-9_-\x02no service configuration \x22%[1]v\x22" + "only consist of A-Za-z0-9_-\x02no service configuration \x22%[1]v\x22" + + "\x02error" - // Total table size 3731 bytes (3KiB); checksum: F7951710 + // Total table size 3745 bytes (3KiB); checksum: 5C167C91 diff --git a/internal/translations/locales/de/out.gotext.json b/internal/translations/locales/de/out.gotext.json index 680505e..06f5f82 100644 --- a/internal/translations/locales/de/out.gotext.json +++ b/internal/translations/locales/de/out.gotext.json @@ -378,6 +378,11 @@ "translatorComment": "Copied from source.", "fuzzy": true }, + { + "id": "error", + "message": "error", + "translation": "" + }, { "id": "done", "message": "done", @@ -462,27 +467,6 @@ } ] }, - { - "id": "Import failed, last reported timestamp was {Arg_1} ({Arg_2})", - "message": "Import failed, last reported timestamp was {Arg_1} ({Arg_2})", - "translation": "Import fehlgeschlagen, letzter Zeitstempel war {Arg_1} ({Arg_2})", - "placeholders": [ - { - "id": "Arg_1", - "string": "%[1]v", - "type": "", - "underlyingType": "interface{}", - "argNum": 1 - }, - { - "id": "Arg_2", - "string": "%[2]s", - "type": "", - "underlyingType": "string", - "argNum": 2 - } - ] - }, { "id": "Imported {ImportCount} of {TotalCount} {Entity} into {TargetName}.", "message": "Imported {ImportCount} of {TotalCount} {Entity} into {TargetName}.", @@ -522,6 +506,27 @@ } ] }, + { + "id": "Import failed, last reported timestamp was {Arg_1} ({Arg_2})", + "message": "Import failed, last reported timestamp was {Arg_1} ({Arg_2})", + "translation": "Import fehlgeschlagen, letzter Zeitstempel war {Arg_1} ({Arg_2})", + "placeholders": [ + { + "id": "Arg_1", + "string": "%[1]v", + "type": "", + "underlyingType": "interface{}", + "argNum": 1 + }, + { + "id": "Arg_2", + "string": "%[2]s", + "type": "", + "underlyingType": "string", + "argNum": 2 + } + ] + }, { "id": "Import log:", "message": "Import log:", diff --git a/internal/translations/locales/en/out.gotext.json b/internal/translations/locales/en/out.gotext.json index eecf359..0ef12a7 100644 --- a/internal/translations/locales/en/out.gotext.json +++ b/internal/translations/locales/en/out.gotext.json @@ -448,6 +448,13 @@ "translatorComment": "Copied from source.", "fuzzy": true }, + { + "id": "error", + "message": "error", + "translation": "error", + "translatorComment": "Copied from source.", + "fuzzy": true + }, { "id": "done", "message": "done", @@ -546,29 +553,6 @@ ], "fuzzy": true }, - { - "id": "Import failed, last reported timestamp was {Arg_1} ({Arg_2})", - "message": "Import failed, last reported timestamp was {Arg_1} ({Arg_2})", - "translation": "Import failed, last reported timestamp was {Arg_1} ({Arg_2})", - "translatorComment": "Copied from source.", - "placeholders": [ - { - "id": "Arg_1", - "string": "%[1]v", - "type": "", - "underlyingType": "interface{}", - "argNum": 1 - }, - { - "id": "Arg_2", - "string": "%[2]s", - "type": "", - "underlyingType": "string", - "argNum": 2 - } - ], - "fuzzy": true - }, { "id": "Imported {ImportCount} of {TotalCount} {Entity} into {TargetName}.", "message": "Imported {ImportCount} of {TotalCount} {Entity} into {TargetName}.", @@ -610,6 +594,29 @@ ], "fuzzy": true }, + { + "id": "Import failed, last reported timestamp was {Arg_1} ({Arg_2})", + "message": "Import failed, last reported timestamp was {Arg_1} ({Arg_2})", + "translation": "Import failed, last reported timestamp was {Arg_1} ({Arg_2})", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Arg_1", + "string": "%[1]v", + "type": "", + "underlyingType": "interface{}", + "argNum": 1 + }, + { + "id": "Arg_2", + "string": "%[2]s", + "type": "", + "underlyingType": "string", + "argNum": 2 + } + ], + "fuzzy": true + }, { "id": "Import log:", "message": "Import log:", From a8517ea2490cb4ca7b5fa87bd5a9300206788da3 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 4 May 2025 13:22:51 +0200 Subject: [PATCH 10/14] funkwhale: fix progress abort on error --- internal/backends/funkwhale/funkwhale.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/backends/funkwhale/funkwhale.go b/internal/backends/funkwhale/funkwhale.go index 3619869..e32a952 100644 --- a/internal/backends/funkwhale/funkwhale.go +++ b/internal/backends/funkwhale/funkwhale.go @@ -119,7 +119,7 @@ out: for { result, err := b.client.GetFavoriteTracks(page, perPage) if err != nil { - progress <- p.Complete() + progress <- p.Abort() results <- models.LovesResult{Error: err} return } From 05f0e8d172a1863c3913f5d0db4ed4f56af2fd43 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 4 May 2025 13:24:12 +0200 Subject: [PATCH 11/14] Change string for aborted progress bar --- internal/cli/progress.go | 2 +- internal/translations/catalog.go | 30 +++++++++---------- .../translations/locales/de/out.gotext.json | 4 +-- .../translations/locales/en/out.gotext.json | 6 ++-- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/internal/cli/progress.go b/internal/cli/progress.go index 6b60697..88339ef 100644 --- a/internal/cli/progress.go +++ b/internal/cli/progress.go @@ -61,7 +61,7 @@ func setupProgressBar(p *mpb.Progress, name string) *mpb.Bar { decor.OnComplete( decor.OnAbort( decor.EwmaETA(decor.ET_STYLE_GO, 0, decor.WC{C: decor.DSyncWidth}), - i18n.Tr("error"), + i18n.Tr("aborted"), ), i18n.Tr("done"), ), diff --git a/internal/translations/catalog.go b/internal/translations/catalog.go index fbe5eef..20f3f35 100644 --- a/internal/translations/catalog.go +++ b/internal/translations/catalog.go @@ -83,17 +83,17 @@ var messageKeyToIndex = map[string]int{ "Visit the URL for authorization: %v": 32, "Yes": 38, "a service with this name already exists": 4, - "backend %s does not implement %s": 13, - "done": 37, - "error": 54, - "exporting": 35, - "importing": 36, - "invalid timestamp string \"%v\"": 49, - "key must only consist of A-Za-z0-9_-": 52, - "no configuration file defined, cannot write config": 51, - "no existing service configurations": 40, - "no service configuration \"%v\"": 53, - "unknown backend \"%s\"": 14, + "aborted": 54, + "backend %s does not implement %s": 13, + "done": 37, + "exporting": 35, + "importing": 36, + "invalid timestamp string \"%v\"": 49, + "key must only consist of A-Za-z0-9_-": 52, + "no configuration file defined, cannot write config": 51, + "no existing service configurations": 40, + "no service configuration \"%v\"": 53, + "unknown backend \"%s\"": 14, } var deIndex = []uint32{ // 56 elements @@ -161,10 +161,10 @@ var enIndex = []uint32{ // 56 elements 0x00000432, 0x00000455, 0x0000045d, 0x00000465, 0x0000048f, 0x000004ad, 0x000004d7, 0x00000510, 0x0000051c, 0x00000529, 0x0000054a, 0x0000056a, - 0x0000059d, 0x000005c2, 0x000005e3, 0x000005e9, + 0x0000059d, 0x000005c2, 0x000005e3, 0x000005eb, } // Size: 248 bytes -const enData string = "" + // Size: 1513 bytes +const enData string = "" + // Size: 1515 bytes "\x04\x01\x09\x00\x0e\x02export: %[1]s\x04\x01\x09\x01\x0a\x0e\x02import:" + " %[1]s\x02Failed reading config: %[1]v\x02Service name\x02a service with" + " this name already exists\x02Saved service %[1]v using backend %[2]v\x02" + @@ -190,6 +190,6 @@ const enData string = "" + // Size: 1513 bytes "\x02invalid timestamp string \x22%[1]v\x22\x02Latest timestamp: %[1]v (%" + "[2]v)\x02no configuration file defined, cannot write config\x02key must " + "only consist of A-Za-z0-9_-\x02no service configuration \x22%[1]v\x22" + - "\x02error" + "\x02aborted" - // Total table size 3745 bytes (3KiB); checksum: 5C167C91 + // Total table size 3747 bytes (3KiB); checksum: 1EAA307C diff --git a/internal/translations/locales/de/out.gotext.json b/internal/translations/locales/de/out.gotext.json index 06f5f82..810655f 100644 --- a/internal/translations/locales/de/out.gotext.json +++ b/internal/translations/locales/de/out.gotext.json @@ -379,8 +379,8 @@ "fuzzy": true }, { - "id": "error", - "message": "error", + "id": "aborted", + "message": "aborted", "translation": "" }, { diff --git a/internal/translations/locales/en/out.gotext.json b/internal/translations/locales/en/out.gotext.json index 0ef12a7..6f6825f 100644 --- a/internal/translations/locales/en/out.gotext.json +++ b/internal/translations/locales/en/out.gotext.json @@ -449,9 +449,9 @@ "fuzzy": true }, { - "id": "error", - "message": "error", - "translation": "error", + "id": "aborted", + "message": "aborted", + "translation": "aborted", "translatorComment": "Copied from source.", "fuzzy": true }, From cb6a534fa184b31bbaf9f0e89c2f41506e020f3b Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 4 May 2025 11:25:07 +0000 Subject: [PATCH 12/14] Translated using Weblate (German) Currently translated at 100.0% (55 of 55 strings) Co-authored-by: Philipp Wolfer Translate-URL: https://translate.uploadedlobster.com/projects/scotty/app/de/ Translation: Scotty/app --- internal/translations/locales/de/out.gotext.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/translations/locales/de/out.gotext.json b/internal/translations/locales/de/out.gotext.json index 810655f..d114352 100644 --- a/internal/translations/locales/de/out.gotext.json +++ b/internal/translations/locales/de/out.gotext.json @@ -367,28 +367,28 @@ { "id": "exporting", "message": "exporting", - "translation": "exportiere", "translatorComment": "Copied from source.", - "fuzzy": true + "fuzzy": true, + "translation": "exportiere" }, { "id": "importing", "message": "importing", - "translation": "importiere", "translatorComment": "Copied from source.", - "fuzzy": true + "fuzzy": true, + "translation": "importiere" }, { "id": "aborted", "message": "aborted", - "translation": "" + "translation": "abgebrochen" }, { "id": "done", "message": "done", - "translation": "fertig", "translatorComment": "Copied from source.", - "fuzzy": true + "fuzzy": true, + "translation": "fertig" }, { "id": "Yes", @@ -617,4 +617,4 @@ ] } ] -} \ No newline at end of file +} From 54fffce1d942757a2c63e07f0a5d0c7b99152333 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 4 May 2025 13:31:44 +0200 Subject: [PATCH 13/14] Update translation files --- internal/translations/catalog.go | 96 +++++------ .../locales/de/messages.gotext.json | 50 +++--- .../translations/locales/de/out.gotext.json | 17 +- .../locales/en/messages.gotext.json | 162 ++++++------------ .../translations/locales/en/out.gotext.json | 162 ++++++------------ 5 files changed, 189 insertions(+), 298 deletions(-) diff --git a/internal/translations/catalog.go b/internal/translations/catalog.go index 20f3f35..f0aaaae 100644 --- a/internal/translations/catalog.go +++ b/internal/translations/catalog.go @@ -42,12 +42,12 @@ var messageKeyToIndex = map[string]int{ "\tbackend: %v": 11, "\texport: %s": 0, "\timport: %s\n": 1, - "%v: %v": 48, + "%v: %v": 49, "Aborted": 8, "Access token": 19, "Access token received, you can use %v now.\n": 34, "Append to file": 21, - "Backend": 42, + "Backend": 43, "Check for duplicate listens on import (slower)": 24, "Client ID": 15, "Client secret": 16, @@ -57,42 +57,42 @@ var messageKeyToIndex = map[string]int{ "Error: OAuth state mismatch": 33, "Failed reading config: %v": 2, "File path": 20, - "From timestamp: %v (%v)": 44, + "From timestamp: %v (%v)": 45, "Ignore listens in incognito mode": 30, "Ignore skipped listens": 27, "Ignored duplicate listen %v: \"%v\" by %v (%v)": 25, - "Import failed, last reported timestamp was %v (%s)": 46, - "Import log:": 47, - "Imported %v of %v %s into %v.": 45, - "Latest timestamp: %v (%v)": 50, + "Import failed, last reported timestamp was %v (%s)": 47, + "Import log:": 48, + "Imported %v of %v %s into %v.": 46, + "Latest timestamp: %v (%v)": 51, "Minimum playback duration for skipped tracks (seconds)": 31, - "No": 39, + "No": 40, "Playlist title": 22, "Saved service %v using backend %v": 5, "Server URL": 17, - "Service": 41, + "Service": 42, "Service \"%v\" deleted\n": 9, "Service name": 3, "Specify a time zone for the listen timestamps": 28, "The backend %v requires authentication. Authenticate now?": 6, "Token received, you can close this window now.": 12, - "Transferring %s from %s to %s…": 43, + "Transferring %s from %s to %s…": 44, "Unique playlist identifier": 23, "Updated service %v using backend %v\n": 10, "User name": 18, "Visit the URL for authorization: %v": 32, - "Yes": 38, + "Yes": 39, "a service with this name already exists": 4, - "aborted": 54, + "aborted": 37, "backend %s does not implement %s": 13, - "done": 37, + "done": 38, "exporting": 35, "importing": 36, - "invalid timestamp string \"%v\"": 49, - "key must only consist of A-Za-z0-9_-": 52, - "no configuration file defined, cannot write config": 51, - "no existing service configurations": 40, - "no service configuration \"%v\"": 53, + "invalid timestamp string \"%v\"": 50, + "key must only consist of A-Za-z0-9_-": 53, + "no configuration file defined, cannot write config": 52, + "no existing service configurations": 41, + "no service configuration \"%v\"": 54, "unknown backend \"%s\"": 14, } @@ -108,14 +108,14 @@ var deIndex = []uint32{ // 56 elements 0x0000037e, 0x000003a4, 0x000003b4, 0x000003da, // Entry 20 - 3F 0x00000418, 0x00000443, 0x0000046d, 0x000004ad, - 0x000004b8, 0x000004c3, 0x000004ca, 0x000004cd, - 0x000004d2, 0x000004fb, 0x00000503, 0x0000050b, - 0x00000534, 0x00000552, 0x0000057d, 0x000005ba, - 0x000005c5, 0x000005d2, 0x000005f6, 0x00000619, - 0x0000066a, 0x000006a1, 0x000006c8, 0x000006c8, + 0x000004b8, 0x000004c3, 0x000004cf, 0x000004d6, + 0x000004d9, 0x000004de, 0x00000507, 0x0000050f, + 0x00000517, 0x00000540, 0x0000055e, 0x00000589, + 0x000005c6, 0x000005d1, 0x000005de, 0x00000602, + 0x00000625, 0x00000676, 0x000006ad, 0x000006d4, } // Size: 248 bytes -const deData string = "" + // Size: 1736 bytes +const deData string = "" + // Size: 1748 bytes "\x04\x01\x09\x00\x0e\x02Export: %[1]s\x04\x01\x09\x01\x0a\x0e\x02Import:" + " %[1]s\x02Fehler beim Lesen der Konfiguration: %[1]v\x02Servicename\x02e" + "in Service mit diesem Namen existiert bereits\x02Service %[1]v mit dem B" + @@ -135,15 +135,15 @@ const deData string = "" + // Size: 1736 bytes "inimale Wiedergabedauer für übersprungene Titel (Sekunden)\x02Zur Anmeld" + "ung folgende URL aufrufen: %[1]v\x02Fehler: OAuth-State stimmt nicht übe" + "rein\x04\x00\x01\x0a;\x02Zugriffstoken erhalten, %[1]v kann jetzt verwen" + - "det werden.\x02exportiere\x02importiere\x02fertig\x02Ja\x02Nein\x02keine" + - " bestehenden Servicekonfigurationen\x02Service\x02Backend\x02Übertrage %" + - "[1]s von %[2]s nach %[3]s…\x02Ab Zeitstempel: %[1]v (%[2]v)\x02%[1]v von" + - " %[2]v %[3]s in %[4]v importiert.\x02Import fehlgeschlagen, letzter Zeit" + - "stempel war %[1]v (%[2]s)\x02Importlog:\x02%[1]v: %[2]v\x02ungültiger Ze" + - "itstempel „%[1]v“\x02Letzter Zeitstempel: %[1]v (%[2]v)\x02keine Konfigu" + - "rationsdatei definiert, Konfiguration kann nicht geschrieben werden\x02S" + - "chlüssel darf nur die Zeichen A-Za-z0-9_- beinhalten\x02keine Servicekon" + - "figuration „%[1]v“" + "det werden.\x02exportiere\x02importiere\x02abgebrochen\x02fertig\x02Ja" + + "\x02Nein\x02keine bestehenden Servicekonfigurationen\x02Service\x02Backe" + + "nd\x02Übertrage %[1]s von %[2]s nach %[3]s…\x02Ab Zeitstempel: %[1]v (%[" + + "2]v)\x02%[1]v von %[2]v %[3]s in %[4]v importiert.\x02Import fehlgeschla" + + "gen, letzter Zeitstempel war %[1]v (%[2]s)\x02Importlog:\x02%[1]v: %[2]v" + + "\x02ungültiger Zeitstempel „%[1]v“\x02Letzter Zeitstempel: %[1]v (%[2]v)" + + "\x02keine Konfigurationsdatei definiert, Konfiguration kann nicht geschr" + + "ieben werden\x02Schlüssel darf nur die Zeichen A-Za-z0-9_- beinhalten" + + "\x02keine Servicekonfiguration „%[1]v“" var enIndex = []uint32{ // 56 elements // Entry 0 - 1F @@ -157,11 +157,11 @@ var enIndex = []uint32{ // 56 elements 0x00000307, 0x00000335, 0x00000344, 0x00000365, // Entry 20 - 3F 0x0000039c, 0x000003c3, 0x000003df, 0x00000412, - 0x0000041c, 0x00000426, 0x0000042b, 0x0000042f, - 0x00000432, 0x00000455, 0x0000045d, 0x00000465, - 0x0000048f, 0x000004ad, 0x000004d7, 0x00000510, - 0x0000051c, 0x00000529, 0x0000054a, 0x0000056a, - 0x0000059d, 0x000005c2, 0x000005e3, 0x000005eb, + 0x0000041c, 0x00000426, 0x0000042e, 0x00000433, + 0x00000437, 0x0000043a, 0x0000045d, 0x00000465, + 0x0000046d, 0x00000497, 0x000004b5, 0x000004df, + 0x00000518, 0x00000524, 0x00000531, 0x00000552, + 0x00000572, 0x000005a5, 0x000005ca, 0x000005eb, } // Size: 248 bytes const enData string = "" + // Size: 1515 bytes @@ -182,14 +182,14 @@ const enData string = "" + // Size: 1515 bytes "mps\x02Directory path\x02Ignore listens in incognito mode\x02Minimum pla" + "yback duration for skipped tracks (seconds)\x02Visit the URL for authori" + "zation: %[1]v\x02Error: OAuth state mismatch\x04\x00\x01\x0a.\x02Access " + - "token received, you can use %[1]v now.\x02exporting\x02importing\x02done" + - "\x02Yes\x02No\x02no existing service configurations\x02Service\x02Backen" + - "d\x02Transferring %[1]s from %[2]s to %[3]s…\x02From timestamp: %[1]v (%" + - "[2]v)\x02Imported %[1]v of %[2]v %[3]s into %[4]v.\x02Import failed, las" + - "t reported timestamp was %[1]v (%[2]s)\x02Import log:\x02%[1]v: %[2]v" + - "\x02invalid timestamp string \x22%[1]v\x22\x02Latest timestamp: %[1]v (%" + - "[2]v)\x02no configuration file defined, cannot write config\x02key must " + - "only consist of A-Za-z0-9_-\x02no service configuration \x22%[1]v\x22" + - "\x02aborted" + "token received, you can use %[1]v now.\x02exporting\x02importing\x02abor" + + "ted\x02done\x02Yes\x02No\x02no existing service configurations\x02Servic" + + "e\x02Backend\x02Transferring %[1]s from %[2]s to %[3]s…\x02From timestam" + + "p: %[1]v (%[2]v)\x02Imported %[1]v of %[2]v %[3]s into %[4]v.\x02Import " + + "failed, last reported timestamp was %[1]v (%[2]s)\x02Import log:\x02%[1]" + + "v: %[2]v\x02invalid timestamp string \x22%[1]v\x22\x02Latest timestamp: " + + "%[1]v (%[2]v)\x02no configuration file defined, cannot write config\x02k" + + "ey must only consist of A-Za-z0-9_-\x02no service configuration \x22%[1]" + + "v\x22" - // Total table size 3747 bytes (3KiB); checksum: 1EAA307C + // Total table size 3759 bytes (3KiB); checksum: 7B4CF967 diff --git a/internal/translations/locales/de/messages.gotext.json b/internal/translations/locales/de/messages.gotext.json index 8cbe44a..b44b7af 100644 --- a/internal/translations/locales/de/messages.gotext.json +++ b/internal/translations/locales/de/messages.gotext.json @@ -368,21 +368,23 @@ "id": "exporting", "message": "exporting", "translatorComment": "Copied from source.", - "fuzzy": true, "translation": "exportiere" }, { "id": "importing", "message": "importing", "translatorComment": "Copied from source.", - "fuzzy": true, "translation": "importiere" }, + { + "id": "aborted", + "message": "aborted", + "translation": "abgebrochen" + }, { "id": "done", "message": "done", "translatorComment": "Copied from source.", - "fuzzy": true, "translation": "fertig" }, { @@ -462,27 +464,6 @@ } ] }, - { - "id": "Import failed, last reported timestamp was {Arg_1} ({Arg_2})", - "message": "Import failed, last reported timestamp was {Arg_1} ({Arg_2})", - "translation": "Import fehlgeschlagen, letzter Zeitstempel war {Arg_1} ({Arg_2})", - "placeholders": [ - { - "id": "Arg_1", - "string": "%[1]v", - "type": "", - "underlyingType": "interface{}", - "argNum": 1 - }, - { - "id": "Arg_2", - "string": "%[2]s", - "type": "", - "underlyingType": "string", - "argNum": 2 - } - ] - }, { "id": "Imported {ImportCount} of {TotalCount} {Entity} into {TargetName}.", "message": "Imported {ImportCount} of {TotalCount} {Entity} into {TargetName}.", @@ -522,6 +503,27 @@ } ] }, + { + "id": "Import failed, last reported timestamp was {Arg_1} ({Arg_2})", + "message": "Import failed, last reported timestamp was {Arg_1} ({Arg_2})", + "translation": "Import fehlgeschlagen, letzter Zeitstempel war {Arg_1} ({Arg_2})", + "placeholders": [ + { + "id": "Arg_1", + "string": "%[1]v", + "type": "", + "underlyingType": "interface{}", + "argNum": 1 + }, + { + "id": "Arg_2", + "string": "%[2]s", + "type": "", + "underlyingType": "string", + "argNum": 2 + } + ] + }, { "id": "Import log:", "message": "Import log:", diff --git a/internal/translations/locales/de/out.gotext.json b/internal/translations/locales/de/out.gotext.json index d114352..863d9c8 100644 --- a/internal/translations/locales/de/out.gotext.json +++ b/internal/translations/locales/de/out.gotext.json @@ -367,16 +367,14 @@ { "id": "exporting", "message": "exporting", - "translatorComment": "Copied from source.", - "fuzzy": true, - "translation": "exportiere" + "translation": "exportiere", + "translatorComment": "Copied from source." }, { "id": "importing", "message": "importing", - "translatorComment": "Copied from source.", - "fuzzy": true, - "translation": "importiere" + "translation": "importiere", + "translatorComment": "Copied from source." }, { "id": "aborted", @@ -386,9 +384,8 @@ { "id": "done", "message": "done", - "translatorComment": "Copied from source.", - "fuzzy": true, - "translation": "fertig" + "translation": "fertig", + "translatorComment": "Copied from source." }, { "id": "Yes", @@ -617,4 +614,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/internal/translations/locales/en/messages.gotext.json b/internal/translations/locales/en/messages.gotext.json index ed62636..878db22 100644 --- a/internal/translations/locales/en/messages.gotext.json +++ b/internal/translations/locales/en/messages.gotext.json @@ -15,8 +15,7 @@ "argNum": 1, "expr": "strings.Join(info.ExportCapabilities, \", \")" } - ], - "fuzzy": true + ] }, { "id": "import: {ImportCapabilities__}", @@ -32,8 +31,7 @@ "argNum": 1, "expr": "strings.Join(info.ImportCapabilities, \", \")" } - ], - "fuzzy": true + ] }, { "id": "Failed reading config: {Err}", @@ -49,22 +47,19 @@ "argNum": 1, "expr": "err" } - ], - "fuzzy": true + ] }, { "id": "Service name", "message": "Service name", "translation": "Service name", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "a service with this name already exists", "message": "a service with this name already exists", "translation": "a service with this name already exists", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Saved service {Name} using backend {Backend}", @@ -88,8 +83,7 @@ "argNum": 2, "expr": "service.Backend" } - ], - "fuzzy": true + ] }, { "id": "The backend {Backend} requires authentication. Authenticate now?", @@ -105,8 +99,7 @@ "argNum": 1, "expr": "service.Backend" } - ], - "fuzzy": true + ] }, { "id": "Delete the service configuration \"{Service}\"?", @@ -122,15 +115,13 @@ "argNum": 1, "expr": "service" } - ], - "fuzzy": true + ] }, { "id": "Aborted", "message": "Aborted", "translation": "Aborted", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Service \"{Name}\" deleted", @@ -146,8 +137,7 @@ "argNum": 1, "expr": "service.Name" } - ], - "fuzzy": true + ] }, { "id": "Updated service {Name} using backend {Backend}", @@ -171,8 +161,7 @@ "argNum": 2, "expr": "service.Backend" } - ], - "fuzzy": true + ] }, { "id": "backend: {Backend}", @@ -188,15 +177,13 @@ "argNum": 1, "expr": "s.Backend" } - ], - "fuzzy": true + ] }, { "id": "Token received, you can close this window now.", "message": "Token received, you can close this window now.", "translation": "Token received, you can close this window now.", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "backend {Backend} does not implement {InterfaceName}", @@ -220,8 +207,7 @@ "argNum": 2, "expr": "interfaceName" } - ], - "fuzzy": true + ] }, { "id": "unknown backend \"{BackendName}\"", @@ -237,78 +223,67 @@ "argNum": 1, "expr": "backendName" } - ], - "fuzzy": true + ] }, { "id": "Client ID", "message": "Client ID", "translation": "Client ID", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Client secret", "message": "Client secret", "translation": "Client secret", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Server URL", "message": "Server URL", "translation": "Server URL", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "User name", "message": "User name", "translation": "User name", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Access token", "message": "Access token", "translation": "Access token", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "File path", "message": "File path", "translation": "File path", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Append to file", "message": "Append to file", "translation": "Append to file", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Playlist title", "message": "Playlist title", "translation": "Playlist title", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Unique playlist identifier", "message": "Unique playlist identifier", "translation": "Unique playlist identifier", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Check for duplicate listens on import (slower)", "message": "Check for duplicate listens on import (slower)", "translation": "Check for duplicate listens on import (slower)", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMBID})", @@ -348,50 +323,43 @@ "argNum": 4, "expr": "l.RecordingMBID" } - ], - "fuzzy": true + ] }, { "id": "Disable auto correction of submitted listens", "message": "Disable auto correction of submitted listens", "translation": "Disable auto correction of submitted listens", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Ignore skipped listens", "message": "Ignore skipped listens", "translation": "Ignore skipped listens", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Specify a time zone for the listen timestamps", "message": "Specify a time zone for the listen timestamps", "translation": "Specify a time zone for the listen timestamps", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Directory path", "message": "Directory path", "translation": "Directory path", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Ignore listens in incognito mode", "message": "Ignore listens in incognito mode", "translation": "Ignore listens in incognito mode", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Minimum playback duration for skipped tracks (seconds)", "message": "Minimum playback duration for skipped tracks (seconds)", "translation": "Minimum playback duration for skipped tracks (seconds)", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Visit the URL for authorization: {URL}", @@ -407,15 +375,13 @@ "argNum": 1, "expr": "authURL.URL" } - ], - "fuzzy": true + ] }, { "id": "Error: OAuth state mismatch", "message": "Error: OAuth state mismatch", "translation": "Error: OAuth state mismatch", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Access token received, you can use {Name} now.", @@ -431,64 +397,55 @@ "argNum": 1, "expr": "service.Name" } - ], - "fuzzy": true + ] }, { "id": "exporting", "message": "exporting", "translation": "exporting", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "importing", "message": "importing", "translation": "importing", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "done", "message": "done", "translation": "done", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Yes", "message": "Yes", "translation": "Yes", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "No", "message": "No", "translation": "No", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "no existing service configurations", "message": "no existing service configurations", "translation": "no existing service configurations", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Service", "message": "Service", "translation": "Service", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Backend", "message": "Backend", "translation": "Backend", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Transferring {Entity} from {SourceName} to {TargetName}…", @@ -520,8 +477,7 @@ "argNum": 3, "expr": "c.targetName" } - ], - "fuzzy": true + ] }, { "id": "From timestamp: {Arg_1} ({Arg_2})", @@ -543,8 +499,7 @@ "underlyingType": "interface{}", "argNum": 2 } - ], - "fuzzy": true + ] }, { "id": "Import failed, last reported timestamp was {Arg_1} ({Arg_2})", @@ -566,8 +521,7 @@ "underlyingType": "string", "argNum": 2 } - ], - "fuzzy": true + ] }, { "id": "Imported {ImportCount} of {TotalCount} {Entity} into {TargetName}.", @@ -607,15 +561,13 @@ "argNum": 4, "expr": "c.targetName" } - ], - "fuzzy": true + ] }, { "id": "Import log:", "message": "Import log:", "translation": "Import log:", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "{Type}: {Message}", @@ -639,8 +591,7 @@ "argNum": 2, "expr": "entry.Message" } - ], - "fuzzy": true + ] }, { "id": "invalid timestamp string \"{FlagValue}\"", @@ -656,8 +607,7 @@ "argNum": 1, "expr": "flagValue" } - ], - "fuzzy": true + ] }, { "id": "Latest timestamp: {Arg_1} ({Arg_2})", @@ -679,22 +629,19 @@ "underlyingType": "interface{}", "argNum": 2 } - ], - "fuzzy": true + ] }, { "id": "no configuration file defined, cannot write config", "message": "no configuration file defined, cannot write config", "translation": "no configuration file defined, cannot write config", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "key must only consist of A-Za-z0-9_-", "message": "key must only consist of A-Za-z0-9_-", "translation": "key must only consist of A-Za-z0-9_-", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "no service configuration \"{Name}\"", @@ -710,8 +657,7 @@ "argNum": 1, "expr": "name" } - ], - "fuzzy": true + ] } ] } diff --git a/internal/translations/locales/en/out.gotext.json b/internal/translations/locales/en/out.gotext.json index 6f6825f..c2e0e84 100644 --- a/internal/translations/locales/en/out.gotext.json +++ b/internal/translations/locales/en/out.gotext.json @@ -15,8 +15,7 @@ "argNum": 1, "expr": "strings.Join(info.ExportCapabilities, \", \")" } - ], - "fuzzy": true + ] }, { "id": "import: {ImportCapabilities__}", @@ -32,8 +31,7 @@ "argNum": 1, "expr": "strings.Join(info.ImportCapabilities, \", \")" } - ], - "fuzzy": true + ] }, { "id": "Failed reading config: {Err}", @@ -49,22 +47,19 @@ "argNum": 1, "expr": "err" } - ], - "fuzzy": true + ] }, { "id": "Service name", "message": "Service name", "translation": "Service name", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "a service with this name already exists", "message": "a service with this name already exists", "translation": "a service with this name already exists", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Saved service {Name} using backend {Backend}", @@ -88,8 +83,7 @@ "argNum": 2, "expr": "service.Backend" } - ], - "fuzzy": true + ] }, { "id": "The backend {Backend} requires authentication. Authenticate now?", @@ -105,8 +99,7 @@ "argNum": 1, "expr": "service.Backend" } - ], - "fuzzy": true + ] }, { "id": "Delete the service configuration \"{Service}\"?", @@ -122,15 +115,13 @@ "argNum": 1, "expr": "service" } - ], - "fuzzy": true + ] }, { "id": "Aborted", "message": "Aborted", "translation": "Aborted", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Service \"{Name}\" deleted", @@ -146,8 +137,7 @@ "argNum": 1, "expr": "service.Name" } - ], - "fuzzy": true + ] }, { "id": "Updated service {Name} using backend {Backend}", @@ -171,8 +161,7 @@ "argNum": 2, "expr": "service.Backend" } - ], - "fuzzy": true + ] }, { "id": "backend: {Backend}", @@ -188,15 +177,13 @@ "argNum": 1, "expr": "s.Backend" } - ], - "fuzzy": true + ] }, { "id": "Token received, you can close this window now.", "message": "Token received, you can close this window now.", "translation": "Token received, you can close this window now.", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "backend {Backend} does not implement {InterfaceName}", @@ -220,8 +207,7 @@ "argNum": 2, "expr": "interfaceName" } - ], - "fuzzy": true + ] }, { "id": "unknown backend \"{BackendName}\"", @@ -237,78 +223,67 @@ "argNum": 1, "expr": "backendName" } - ], - "fuzzy": true + ] }, { "id": "Client ID", "message": "Client ID", "translation": "Client ID", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Client secret", "message": "Client secret", "translation": "Client secret", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Server URL", "message": "Server URL", "translation": "Server URL", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "User name", "message": "User name", "translation": "User name", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Access token", "message": "Access token", "translation": "Access token", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "File path", "message": "File path", "translation": "File path", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Append to file", "message": "Append to file", "translation": "Append to file", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Playlist title", "message": "Playlist title", "translation": "Playlist title", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Unique playlist identifier", "message": "Unique playlist identifier", "translation": "Unique playlist identifier", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Check for duplicate listens on import (slower)", "message": "Check for duplicate listens on import (slower)", "translation": "Check for duplicate listens on import (slower)", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Ignored duplicate listen {ListenedAt}: \"{TrackName}\" by {ArtistName} ({RecordingMBID})", @@ -348,50 +323,43 @@ "argNum": 4, "expr": "l.RecordingMBID" } - ], - "fuzzy": true + ] }, { "id": "Disable auto correction of submitted listens", "message": "Disable auto correction of submitted listens", "translation": "Disable auto correction of submitted listens", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Ignore skipped listens", "message": "Ignore skipped listens", "translation": "Ignore skipped listens", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Specify a time zone for the listen timestamps", "message": "Specify a time zone for the listen timestamps", "translation": "Specify a time zone for the listen timestamps", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Directory path", "message": "Directory path", "translation": "Directory path", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Ignore listens in incognito mode", "message": "Ignore listens in incognito mode", "translation": "Ignore listens in incognito mode", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Minimum playback duration for skipped tracks (seconds)", "message": "Minimum playback duration for skipped tracks (seconds)", "translation": "Minimum playback duration for skipped tracks (seconds)", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Visit the URL for authorization: {URL}", @@ -407,15 +375,13 @@ "argNum": 1, "expr": "authURL.URL" } - ], - "fuzzy": true + ] }, { "id": "Error: OAuth state mismatch", "message": "Error: OAuth state mismatch", "translation": "Error: OAuth state mismatch", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Access token received, you can use {Name} now.", @@ -431,22 +397,19 @@ "argNum": 1, "expr": "service.Name" } - ], - "fuzzy": true + ] }, { "id": "exporting", "message": "exporting", "translation": "exporting", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "importing", "message": "importing", "translation": "importing", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "aborted", @@ -459,43 +422,37 @@ "id": "done", "message": "done", "translation": "done", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Yes", "message": "Yes", "translation": "Yes", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "No", "message": "No", "translation": "No", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "no existing service configurations", "message": "no existing service configurations", "translation": "no existing service configurations", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Service", "message": "Service", "translation": "Service", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Backend", "message": "Backend", "translation": "Backend", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "Transferring {Entity} from {SourceName} to {TargetName}…", @@ -527,8 +484,7 @@ "argNum": 3, "expr": "c.targetName" } - ], - "fuzzy": true + ] }, { "id": "From timestamp: {Arg_1} ({Arg_2})", @@ -550,8 +506,7 @@ "underlyingType": "interface{}", "argNum": 2 } - ], - "fuzzy": true + ] }, { "id": "Imported {ImportCount} of {TotalCount} {Entity} into {TargetName}.", @@ -591,8 +546,7 @@ "argNum": 4, "expr": "c.targetName" } - ], - "fuzzy": true + ] }, { "id": "Import failed, last reported timestamp was {Arg_1} ({Arg_2})", @@ -614,15 +568,13 @@ "underlyingType": "string", "argNum": 2 } - ], - "fuzzy": true + ] }, { "id": "Import log:", "message": "Import log:", "translation": "Import log:", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "{Type}: {Message}", @@ -646,8 +598,7 @@ "argNum": 2, "expr": "entry.Message" } - ], - "fuzzy": true + ] }, { "id": "invalid timestamp string \"{FlagValue}\"", @@ -663,8 +614,7 @@ "argNum": 1, "expr": "flagValue" } - ], - "fuzzy": true + ] }, { "id": "Latest timestamp: {Arg_1} ({Arg_2})", @@ -686,22 +636,19 @@ "underlyingType": "interface{}", "argNum": 2 } - ], - "fuzzy": true + ] }, { "id": "no configuration file defined, cannot write config", "message": "no configuration file defined, cannot write config", "translation": "no configuration file defined, cannot write config", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "key must only consist of A-Za-z0-9_-", "message": "key must only consist of A-Za-z0-9_-", "translation": "key must only consist of A-Za-z0-9_-", - "translatorComment": "Copied from source.", - "fuzzy": true + "translatorComment": "Copied from source." }, { "id": "no service configuration \"{Name}\"", @@ -717,8 +664,7 @@ "argNum": 1, "expr": "name" } - ], - "fuzzy": true + ] } ] } \ No newline at end of file From 1f48abc2842e30861c5209271626866c1441da65 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 4 May 2025 15:18:14 +0200 Subject: [PATCH 14/14] Fixed timestamp displayed after import not being the updated one --- internal/cli/transfer.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/cli/transfer.go b/internal/cli/transfer.go index 83bad2a..0391d0d 100644 --- a/internal/cli/transfer.go +++ b/internal/cli/transfer.go @@ -124,7 +124,7 @@ func (c *TransferCmd[E, I, R]) Transfer(exp backends.ExportProcessor[R], imp bac progress.Wait() // Update timestamp - err = c.updateTimestamp(result, timestamp) + err = c.updateTimestamp(&result, timestamp) if err != nil { return err } @@ -174,7 +174,7 @@ func (c *TransferCmd[E, I, R]) timestamp() (time.Time, error) { return time.Time{}, errors.New(i18n.Tr("invalid timestamp string \"%v\"", flagValue)) } -func (c *TransferCmd[E, I, R]) updateTimestamp(result models.ImportResult, oldTimestamp time.Time) error { +func (c *TransferCmd[E, I, R]) updateTimestamp(result *models.ImportResult, oldTimestamp time.Time) error { if oldTimestamp.After(result.LastTimestamp) { result.LastTimestamp = oldTimestamp }