From 8661075975ecc82d8c980f4ea16b6859ff4a337c Mon Sep 17 00:00:00 2001 From: Philipp Wolfer <ph.wolfer@gmail.com> Date: Thu, 9 Nov 2023 14:35:10 +0100 Subject: [PATCH] Basic backend structure and listen transfer implementation --- .gitignore | 1 + backends/base.go | 79 ++++++++++++++++++++++++++++++++++++++++ backends/dump.go | 49 +++++++++++++++++++++++++ backends/models.go | 58 +++++++++++++++++++++++++++++ backends/scrobblerlog.go | 43 ++++++++++++++++++++++ cmd/listens.go | 25 ++++++++++++- 6 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 backends/base.go create mode 100644 backends/dump.go create mode 100644 backends/models.go create mode 100644 backends/scrobblerlog.go diff --git a/.gitignore b/.gitignore index a376f60..01ad3ad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Common files to ignore *~ +.vscode/ # Local config (for testing) /scotty.toml diff --git a/backends/base.go b/backends/base.go new file mode 100644 index 0000000..3776edc --- /dev/null +++ b/backends/base.go @@ -0,0 +1,79 @@ +/* +Copyright © 2023 Philipp Wolfer <phw@uploadedlobster.com> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package backends + +import ( + "errors" + "fmt" + "reflect" + "time" + + "github.com/spf13/viper" +) + +type Backend interface { + FromConfig(config *viper.Viper) Backend +} + +type ListenExport interface { + ExportListens(oldestTimestamp time.Time) ([]Listen, error) +} + +type ListenImport interface { + ImportListens(listens []Listen, oldestTimestamp time.Time) (ImportResult, error) +} + +type ImportResult struct { + Count int + LastTimestamp time.Time +} + +func resolveBackend(config *viper.Viper) (string, Backend, error) { + backendName := config.GetString("backend") + fmt.Printf("requested backend %s\n", backendName) + backendType := knownBackends[backendName] + if backendType == nil { + return backendName, nil, errors.New(fmt.Sprintf("Unknown backend %s", backendName)) + } + return backendName, backendType().FromConfig(config), nil +} + +func ResolveBackend[T interface{}](config *viper.Viper) (T, error) { + expectedInterface := reflect.TypeOf((*T)(nil)).Elem() + backendName, backend, err := resolveBackend(config) + var result T + if err != nil { + return result, err + } + if backend != nil && reflect.TypeOf(backend).Implements(expectedInterface) { + result = backend.(T) + } else { + err = errors.New( + fmt.Sprintf("Backend %s does not implement %s", backendName, expectedInterface.String())) + } + return result, err +} + +var knownBackends = map[string]func() Backend{ + "dump": func() Backend { return &DumpBackend{} }, + "scrobbler-log": func() Backend { return &ScrobblerLogBackend{} }, +} diff --git a/backends/dump.go b/backends/dump.go new file mode 100644 index 0000000..6649db4 --- /dev/null +++ b/backends/dump.go @@ -0,0 +1,49 @@ +/* +Copyright © 2023 Philipp Wolfer <phw@uploadedlobster.com> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package backends + +import ( + "fmt" + "time" + + "github.com/spf13/viper" +) + +type DumpBackend struct{} + +func (b DumpBackend) FromConfig(config *viper.Viper) Backend { + return b +} + +func (b DumpBackend) ImportListens(listens []Listen, oldestTimestamp time.Time) (ImportResult, error) { + result := ImportResult{ + Count: len(listens), + LastTimestamp: oldestTimestamp, + } + if result.Count > 0 { + result.LastTimestamp = listens[len(listens)-1].ListenedAt + } + for _, listen := range listens { + fmt.Printf("Listen: \"%s\" by %s\n", listen.TrackName, listen.ArtistName()) + } + return result, nil +} diff --git a/backends/models.go b/backends/models.go new file mode 100644 index 0000000..b20b938 --- /dev/null +++ b/backends/models.go @@ -0,0 +1,58 @@ +/* +Copyright © 2023 Philipp Wolfer <phw@uploadedlobster.com> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package backends + +import ( + "strings" + "time" +) + +type MBID string + +type AdditionalInfo map[string]string + +type Track struct { + TrackName string + ReleaseName string + ArtistNames []string + TrackNumber int + Duration time.Duration + Isrc string + RecordingMbid MBID + ReleaseMbid MBID + ReleaseGroupMbid MBID + ArtistMbids []MBID + WorkMbids []MBID + Tags []string + AdditionalInfo AdditionalInfo +} + +func (t Track) ArtistName() string { + return strings.Join(t.ArtistNames, ", ") +} + +type Listen struct { + Track + ListenedAt time.Time + ListenDuration time.Duration + UserName string +} diff --git a/backends/scrobblerlog.go b/backends/scrobblerlog.go new file mode 100644 index 0000000..3fa8f17 --- /dev/null +++ b/backends/scrobblerlog.go @@ -0,0 +1,43 @@ +/* +Copyright © 2023 Philipp Wolfer <phw@uploadedlobster.com> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package backends + +import ( + "time" + + "github.com/spf13/viper" +) + +type ScrobblerLogBackend struct { + filePath string + includeSkipped bool +} + +func (b ScrobblerLogBackend) FromConfig(config *viper.Viper) Backend { + b.filePath = config.GetString("file-path") + b.includeSkipped = config.GetBool("include-skipped") + return b +} + +func (b ScrobblerLogBackend) ExportListens(oldestTimestamp time.Time) ([]Listen, error) { + return nil, nil +} diff --git a/cmd/listens.go b/cmd/listens.go index ea64963..02b51d2 100644 --- a/cmd/listens.go +++ b/cmd/listens.go @@ -23,8 +23,11 @@ package cmd import ( "fmt" + "time" "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.uploadedlobster.com/scotty/backends" ) // listensCmd represents the listens command @@ -34,12 +37,32 @@ var listensCmd = &cobra.Command{ Long: `Transfers listens between two configured backends.`, Run: func(cmd *cobra.Command, args []string) { fmt.Println("listens called") + sourceConfig := getConfigFromFlag(cmd, "from") + targetConfig := getConfigFromFlag(cmd, "to") + exportBackend, err := backends.ResolveBackend[backends.ListenExport](sourceConfig) + cobra.CheckErr(err) + importBackend, err := backends.ResolveBackend[backends.ListenImport](targetConfig) + cobra.CheckErr(err) + timestamp := time.Unix(0, 0) + listens, err := exportBackend.ExportListens(timestamp) + cobra.CheckErr(err) + result, err := importBackend.ImportListens(listens, timestamp) + cobra.CheckErr(err) + fmt.Printf("Imported %v listens (last timestamp %v)\n", result.Count, result.LastTimestamp) }, } +func getConfigFromFlag(cmd *cobra.Command, flagName string) *viper.Viper { + configName := cmd.Flag(flagName).Value.String() + config := viper.Sub(configName) + if config == nil { + cobra.CheckErr(fmt.Sprintf("Invalid source configuration \"%s\"", configName)) + } + return config +} + func init() { beamCmd.AddCommand(listensCmd) - // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command