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