mirror of
https://git.sr.ht/~phw/scotty
synced 2025-04-29 21:27:05 +02:00
Restructured code, moved all modules into internal
For now all modules are considered internal. This might change later
This commit is contained in:
parent
f94e0f1e85
commit
857661ebf9
76 changed files with 121 additions and 68 deletions
95
internal/models/interfaces.go
Normal file
95
internal/models/interfaces.go
Normal file
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
Copyright © 2023 Philipp Wolfer <phw@uploadedlobster.com>
|
||||
|
||||
This file is part of Scotty.
|
||||
|
||||
Scotty is free software: you can redistribute it and/or modify it under the
|
||||
terms of the GNU General Public License as published by the Free Software
|
||||
Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
Scotty is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with
|
||||
Scotty. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package models
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"go.uploadedlobster.com/scotty/internal/auth"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// A listen service backend.
|
||||
// All listen services must implement this interface.
|
||||
type Backend interface {
|
||||
// Return the name of the interface
|
||||
Name() string
|
||||
|
||||
// Initialize the backend from a config.
|
||||
FromConfig(config *viper.Viper) Backend
|
||||
}
|
||||
|
||||
type ImportBackend interface {
|
||||
Backend
|
||||
|
||||
// If the backend needs to setup resources before starting to import,
|
||||
// this can be done here.
|
||||
StartImport() error
|
||||
|
||||
// The implementation can perform all steps here to finalize the
|
||||
// export/import and free used resources.
|
||||
FinishImport() error
|
||||
}
|
||||
|
||||
// Must be implemented by services supporting the export of listens.
|
||||
type ListensExport interface {
|
||||
Backend
|
||||
|
||||
// Returns a list of all listens newer then oldestTimestamp.
|
||||
// The returned list of listens is supposed to be ordered by the
|
||||
// Listen.ListenedAt timestamp, with the oldest entry first.
|
||||
ExportListens(oldestTimestamp time.Time, results chan ListensResult, progress chan Progress)
|
||||
}
|
||||
|
||||
// Must be implemented by services supporting the import of listens.
|
||||
type ListensImport interface {
|
||||
ImportBackend
|
||||
|
||||
// Imports the given list of listens.
|
||||
ImportListens(export ListensResult, importResult ImportResult, progress chan Progress) (ImportResult, error)
|
||||
}
|
||||
|
||||
// Must be implemented by services supporting the export of loves.
|
||||
type LovesExport interface {
|
||||
Backend
|
||||
|
||||
// Returns a list of all loves newer then oldestTimestamp.
|
||||
// The returned list of listens is supposed to be ordered by the
|
||||
// Love.Created timestamp, with the oldest entry first.
|
||||
ExportLoves(oldestTimestamp time.Time, results chan LovesResult, progress chan Progress)
|
||||
}
|
||||
|
||||
// Must be implemented by services supporting the import of loves.
|
||||
type LovesImport interface {
|
||||
ImportBackend
|
||||
|
||||
// Imports the given list of loves.
|
||||
ImportLoves(export LovesResult, importResult ImportResult, progress chan Progress) (ImportResult, error)
|
||||
}
|
||||
|
||||
// Must be implemented by backends requiring OAuth2 authentication
|
||||
type OAuth2Authenticator interface {
|
||||
Backend
|
||||
|
||||
// Returns OAuth2 config suitable for this backend
|
||||
OAuth2Strategy(redirectUrl *url.URL) auth.OAuth2Strategy
|
||||
|
||||
// Setup the OAuth2 client
|
||||
OAuth2Setup(token oauth2.TokenSource) error
|
||||
}
|
202
internal/models/models.go
Normal file
202
internal/models/models.go
Normal file
|
@ -0,0 +1,202 @@
|
|||
/*
|
||||
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 models
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type MBID string
|
||||
|
||||
type AdditionalInfo map[string]any
|
||||
|
||||
type Track struct {
|
||||
TrackName string
|
||||
ReleaseName string
|
||||
ArtistNames []string
|
||||
TrackNumber int
|
||||
DiscNumber 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, ", ")
|
||||
}
|
||||
|
||||
// Updates AdditionalInfo to have standard fields as defined by ListenBrainz
|
||||
func (t *Track) FillAdditionalInfo() {
|
||||
if t.AdditionalInfo == nil {
|
||||
t.AdditionalInfo = make(AdditionalInfo, 5)
|
||||
}
|
||||
if t.RecordingMbid != "" {
|
||||
t.AdditionalInfo["recording_mbid"] = t.RecordingMbid
|
||||
}
|
||||
if t.ReleaseGroupMbid != "" {
|
||||
t.AdditionalInfo["release_group_mbid"] = t.ReleaseGroupMbid
|
||||
}
|
||||
if t.ReleaseMbid != "" {
|
||||
t.AdditionalInfo["release_mbid"] = t.ReleaseMbid
|
||||
}
|
||||
if len(t.ArtistMbids) > 0 {
|
||||
t.AdditionalInfo["artist_mbids"] = t.ArtistMbids
|
||||
}
|
||||
if len(t.WorkMbids) > 0 {
|
||||
t.AdditionalInfo["work_mbids"] = t.WorkMbids
|
||||
}
|
||||
if t.ISRC != "" {
|
||||
t.AdditionalInfo["isrc"] = t.ISRC
|
||||
}
|
||||
if t.TrackNumber != 0 {
|
||||
t.AdditionalInfo["tracknumber"] = t.TrackNumber
|
||||
}
|
||||
if t.DiscNumber != 0 {
|
||||
t.AdditionalInfo["discnumber"] = t.DiscNumber
|
||||
}
|
||||
if t.Duration != 0 {
|
||||
rounded := t.Duration.Round(time.Second)
|
||||
if t.Duration == rounded {
|
||||
t.AdditionalInfo["duration"] = int64(t.Duration.Seconds())
|
||||
} else {
|
||||
t.AdditionalInfo["duration_ms"] = t.Duration.Milliseconds()
|
||||
}
|
||||
}
|
||||
if len(t.Tags) > 0 {
|
||||
t.AdditionalInfo["tags"] = t.Tags
|
||||
}
|
||||
}
|
||||
|
||||
type Listen struct {
|
||||
Track
|
||||
ListenedAt time.Time
|
||||
PlaybackDuration time.Duration
|
||||
UserName string
|
||||
}
|
||||
|
||||
type Love struct {
|
||||
Track
|
||||
Created time.Time
|
||||
UserName string
|
||||
RecordingMbid MBID
|
||||
RecordingMsid MBID
|
||||
}
|
||||
|
||||
type ListensList []Listen
|
||||
|
||||
// Returns a new ListensList with only elements that are newer than t.
|
||||
func (l ListensList) NewerThan(t time.Time) ListensList {
|
||||
result := make(ListensList, 0, len(l))
|
||||
for _, item := range l {
|
||||
if item.ListenedAt.Unix() > t.Unix() {
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (l ListensList) Len() int {
|
||||
return len(l)
|
||||
}
|
||||
|
||||
func (l ListensList) Less(i, j int) bool {
|
||||
return l[i].ListenedAt.Unix() < l[j].ListenedAt.Unix()
|
||||
}
|
||||
|
||||
func (l ListensList) Swap(i, j int) {
|
||||
l[i], l[j] = l[j], l[i]
|
||||
}
|
||||
|
||||
type LovesList []Love
|
||||
|
||||
func (l LovesList) Len() int {
|
||||
return len(l)
|
||||
}
|
||||
|
||||
func (l LovesList) Less(i, j int) bool {
|
||||
return l[i].Created.Unix() < l[j].Created.Unix()
|
||||
}
|
||||
|
||||
func (l LovesList) Swap(i, j int) {
|
||||
l[i], l[j] = l[j], l[i]
|
||||
}
|
||||
|
||||
type ListensResult struct {
|
||||
Total int
|
||||
Listens ListensList
|
||||
OldestTimestamp time.Time
|
||||
Error error
|
||||
}
|
||||
|
||||
type LovesResult struct {
|
||||
Total int
|
||||
Loves LovesList
|
||||
Error error
|
||||
}
|
||||
|
||||
type ImportResult struct {
|
||||
TotalCount int
|
||||
ImportCount int
|
||||
LastTimestamp time.Time
|
||||
ImportErrors []string
|
||||
|
||||
// Error is only set if an unrecoverable import error occurred
|
||||
Error error
|
||||
}
|
||||
|
||||
// Sets LastTimestamp to newTime, if newTime is newer than LastTimestamp
|
||||
func (i *ImportResult) UpdateTimestamp(newTime time.Time) {
|
||||
if newTime.Unix() > i.LastTimestamp.Unix() {
|
||||
i.LastTimestamp = newTime
|
||||
}
|
||||
}
|
||||
|
||||
func (i *ImportResult) Update(from ImportResult) {
|
||||
i.TotalCount = from.TotalCount
|
||||
i.ImportCount = from.ImportCount
|
||||
i.UpdateTimestamp(from.LastTimestamp)
|
||||
}
|
||||
|
||||
type Progress struct {
|
||||
Total int64
|
||||
Elapsed int64
|
||||
Completed bool
|
||||
}
|
||||
|
||||
func (p Progress) FromImportResult(result ImportResult) Progress {
|
||||
p.Total = int64(result.TotalCount)
|
||||
p.Elapsed = int64(result.ImportCount)
|
||||
return p
|
||||
}
|
||||
|
||||
func (p Progress) Complete() Progress {
|
||||
p.Total = p.Elapsed
|
||||
p.Completed = true
|
||||
return p
|
||||
}
|
145
internal/models/models_test.go
Normal file
145
internal/models/models_test.go
Normal file
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
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 models_test
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uploadedlobster.com/scotty/internal/models"
|
||||
)
|
||||
|
||||
func TestTrackArtistName(t *testing.T) {
|
||||
track := models.Track{
|
||||
ArtistNames: []string{
|
||||
"Foo",
|
||||
"Bar",
|
||||
"Baz",
|
||||
},
|
||||
}
|
||||
assert.Equal(t, "Foo, Bar, Baz", track.ArtistName())
|
||||
}
|
||||
|
||||
func TestTrackFillAdditionalInfo(t *testing.T) {
|
||||
track := models.Track{
|
||||
RecordingMbid: models.MBID("c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12"),
|
||||
ReleaseGroupMbid: models.MBID("80aca1ee-aa51-41be-9f75-024710d92ff4"),
|
||||
ReleaseMbid: models.MBID("aa1ea1ac-7ec4-4542-a494-105afbfe547d"),
|
||||
ArtistMbids: []models.MBID{"24412926-c7bd-48e8-afad-8a285b42e131"},
|
||||
WorkMbids: []models.MBID{"c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12"},
|
||||
TrackNumber: 5,
|
||||
DiscNumber: 1,
|
||||
Duration: time.Duration(413787 * time.Millisecond),
|
||||
ISRC: "DES561620801",
|
||||
Tags: []string{"rock", "psychedelic rock"},
|
||||
}
|
||||
track.FillAdditionalInfo()
|
||||
i := track.AdditionalInfo
|
||||
assert := assert.New(t)
|
||||
assert.Equal(track.RecordingMbid, i["recording_mbid"])
|
||||
assert.Equal(track.ReleaseGroupMbid, i["release_group_mbid"])
|
||||
assert.Equal(track.ReleaseMbid, i["release_mbid"])
|
||||
assert.Equal(track.ArtistMbids, i["artist_mbids"])
|
||||
assert.Equal(track.WorkMbids, i["work_mbids"])
|
||||
assert.Equal(track.TrackNumber, i["tracknumber"])
|
||||
assert.Equal(track.DiscNumber, i["discnumber"])
|
||||
assert.Equal(track.Duration.Milliseconds(), i["duration_ms"])
|
||||
assert.Equal(track.ISRC, i["isrc"])
|
||||
assert.Equal(track.Tags, i["tags"])
|
||||
}
|
||||
|
||||
func TestTrackFillAdditionalInfoRoundSecond(t *testing.T) {
|
||||
ts := models.Track{Duration: time.Duration(123000 * time.Millisecond)}
|
||||
ts.FillAdditionalInfo()
|
||||
assert.Equal(t, int64(123), ts.AdditionalInfo["duration"])
|
||||
assert.Equal(t, nil, ts.AdditionalInfo["duration_ms"])
|
||||
tms := models.Track{Duration: time.Duration(123001 * time.Millisecond)}
|
||||
tms.FillAdditionalInfo()
|
||||
assert.Equal(t, nil, tms.AdditionalInfo["duration"])
|
||||
assert.Equal(t, int64(123001), tms.AdditionalInfo["duration_ms"])
|
||||
}
|
||||
|
||||
func TestListensListNewerThan(t *testing.T) {
|
||||
listen1 := models.Listen{ListenedAt: time.Unix(3, 0)}
|
||||
listen2 := models.Listen{ListenedAt: time.Unix(0, 0)}
|
||||
listen3 := models.Listen{ListenedAt: time.Unix(2, 0)}
|
||||
list := models.ListensList{listen1, listen2, listen3}
|
||||
sort.Sort(list)
|
||||
assert.Equal(t, listen1, list[2])
|
||||
assert.Equal(t, listen2, list[0])
|
||||
assert.Equal(t, listen3, list[1])
|
||||
}
|
||||
|
||||
func TestListensListSort(t *testing.T) {
|
||||
now := time.Now()
|
||||
listen1 := models.Listen{UserName: "l1", ListenedAt: now.Add(-1 * time.Hour)}
|
||||
listen2 := models.Listen{UserName: "l2", ListenedAt: now}
|
||||
listen3 := models.Listen{UserName: "l3", ListenedAt: now.Add(1 * time.Hour)}
|
||||
listen4 := models.Listen{UserName: "l4", ListenedAt: now.Add(2 * time.Hour)}
|
||||
list := models.ListensList{listen1, listen2, listen3, listen4}
|
||||
newList := list.NewerThan(now)
|
||||
require.Len(t, newList, 2)
|
||||
assert.Equal(t, listen3, newList[0])
|
||||
assert.Equal(t, listen4, newList[1])
|
||||
}
|
||||
|
||||
func TestLovesListSort(t *testing.T) {
|
||||
love1 := models.Love{Created: time.Unix(3, 0)}
|
||||
love2 := models.Love{Created: time.Unix(0, 0)}
|
||||
love3 := models.Love{Created: time.Unix(2, 0)}
|
||||
list := models.LovesList{love1, love2, love3}
|
||||
sort.Sort(list)
|
||||
assert.Equal(t, love1, list[2])
|
||||
assert.Equal(t, love2, list[0])
|
||||
assert.Equal(t, love3, list[1])
|
||||
}
|
||||
|
||||
func TestImportResultUpdate(t *testing.T) {
|
||||
result := models.ImportResult{
|
||||
TotalCount: 100,
|
||||
ImportCount: 20,
|
||||
LastTimestamp: time.Now(),
|
||||
}
|
||||
newResult := models.ImportResult{
|
||||
TotalCount: 120,
|
||||
ImportCount: 50,
|
||||
LastTimestamp: time.Now().Add(1 * time.Hour),
|
||||
}
|
||||
result.Update(newResult)
|
||||
assert.Equal(t, 120, result.TotalCount)
|
||||
assert.Equal(t, 50, result.ImportCount)
|
||||
assert.Equal(t, newResult.LastTimestamp, result.LastTimestamp)
|
||||
}
|
||||
|
||||
func TestImportResultUpdateTimestamp(t *testing.T) {
|
||||
timestamp := time.Now()
|
||||
i := models.ImportResult{LastTimestamp: timestamp}
|
||||
newTimestamp := time.Now().Add(-time.Duration(2 * time.Second))
|
||||
i.UpdateTimestamp(newTimestamp)
|
||||
assert.Equalf(t, timestamp, i.LastTimestamp, "Expected older timestamp is kept")
|
||||
newTimestamp = time.Now().Add(time.Duration(2 * time.Second))
|
||||
i.UpdateTimestamp(newTimestamp)
|
||||
assert.Equalf(t, newTimestamp, i.LastTimestamp, "Updated timestamp expected")
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue