-- cgit v1.2.3 From c615879aa950d3145b5fa83c8d2cce318aa57338 Mon Sep 17 00:00:00 2001 From: Franck Cuny Date: Sat, 9 Oct 2021 13:34:31 -0700 Subject: Add README.org, LICENSE.txt --- LICENSE.txt | 20 ++++++++++++++++++++ README.org | 1 + 2 files changed, 21 insertions(+) create mode 100644 LICENSE.txt create mode 100644 README.org diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..e614d87 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,20 @@ +Copyright (c) 2021 Franck Cuny + +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. diff --git a/README.org b/README.org new file mode 100644 index 0000000..2d06ad9 --- /dev/null +++ b/README.org @@ -0,0 +1 @@ +#+TITLE: mpd-stats \ No newline at end of file -- cgit v1.2.3 From dabb1c41cd6b1526cb6560bbb9c70a55303fdf4d Mon Sep 17 00:00:00 2001 From: Franck Cuny Date: Sat, 9 Oct 2021 13:36:04 -0700 Subject: add initial go.mod --- go.mod | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 go.mod diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..fc37193 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module golang.fcuny.net/mpd-stats + +go 1.17 -- cgit v1.2.3 From dc080550b5fe5cf6cc129463dc0e7330219d3da3 Mon Sep 17 00:00:00 2001 From: Franck Cuny Date: Sat, 9 Oct 2021 13:45:11 -0700 Subject: README: add some information about logging Describe what is being collected. --- README.org | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/README.org b/README.org index 2d06ad9..8765e59 100644 --- a/README.org +++ b/README.org @@ -1 +1,14 @@ -#+TITLE: mpd-stats \ No newline at end of file +#+TITLE: mpd-stats + +Log played songs to extract statistics. This is similar to what libre.fm used to do, but locally. + +* Logging +Collect logs from mpd. A log record is composed of the following fields: +- id: UUID +- song's name: the name of the song +- song's album: the name of the album +- song's artist: the name of the artist +- song's duration: the duration of the song +- date: date the song was played + +The logs are recorded in a database (sqlite3 to start). -- cgit v1.2.3 From 28dd6b6ae32e0cccdaf6486fb76c6b7f98647f27 Mon Sep 17 00:00:00 2001 From: Franck Cuny Date: Sat, 9 Oct 2021 17:20:22 -0700 Subject: scrobbler: watch for events and print song details We create a module "mpd" to interact with our MPD instance. For now we only have a single function to create a new client, which creates an actual client for mpd (and we ping the instance every 30 seconds), and a watcher to receive new events. The tool "scrobbler" then wait for new events and display songs information. --- cmd/mpd-scrobbler/main.go | 35 +++++++++++++++++++++++++++++++++++ go.mod | 2 ++ go.sum | 2 ++ internal/mpd/mpd.go | 38 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 77 insertions(+) create mode 100644 cmd/mpd-scrobbler/main.go create mode 100644 go.sum create mode 100644 internal/mpd/mpd.go diff --git a/cmd/mpd-scrobbler/main.go b/cmd/mpd-scrobbler/main.go new file mode 100644 index 0000000..9929225 --- /dev/null +++ b/cmd/mpd-scrobbler/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "log" + + "golang.fcuny.net/mpd-stats/internal/mpd" +) + +func main() { + net := "tcp" + addr := "localhost:6600" + + c, err := mpd.NewMPD(net, addr) + if err != nil { + log.Fatalf("failed to create a client: %v", err) + } + + defer c.Watcher.Close() + defer c.Client.Close() + + for { + e := <-c.Watcher.Event + if e != "" { + attrs, err := c.Client.CurrentSong() + if err != nil { + log.Fatalf("could not get current song: %v", err) + } + currentAlbum := attrs["Album"] + artist := attrs["Artist"] + song := attrs["Title"] + duration := attrs["duration"] + log.Printf("we're playing %s/%s/%s [%s]\n", artist, currentAlbum, song, duration) + } + } +} diff --git a/go.mod b/go.mod index fc37193..cafa7bc 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module golang.fcuny.net/mpd-stats go 1.17 + +require github.com/fhs/gompd/v2 v2.2.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1c3cda7 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/fhs/gompd/v2 v2.2.0 h1:zdSYAAOzQ5cCCgYa5CoXkL0Vr0Cqb/b5JmTobirLc90= +github.com/fhs/gompd/v2 v2.2.0/go.mod h1:nNdZtcpD5VpmzZbRl5rV6RhxeMmAWTxEsSIMBkmMIy4= diff --git a/internal/mpd/mpd.go b/internal/mpd/mpd.go new file mode 100644 index 0000000..8991adc --- /dev/null +++ b/internal/mpd/mpd.go @@ -0,0 +1,38 @@ +package mpd + +import ( + "log" + "time" + + "github.com/fhs/gompd/v2/mpd" +) + +type player struct { + Watcher *mpd.Watcher + Client *mpd.Client +} + +func NewMPD(net string, addr string) (*player, error) { + var ( + p player + err error + ) + + p.Watcher, err = mpd.NewWatcher(net, addr, "", "player") + if err != nil { + log.Fatalf("failed to create a watcher: %v", err) + } + + p.Client, err = mpd.Dial(net, addr) + if err != nil { + log.Fatalf("failed to start mpd client: %v", err) + } + + go func() { + for range time.Tick(30 * time.Second) { + p.Client.Ping() + } + }() + + return &p, nil +} -- cgit v1.2.3 From 49480621b580a4676109e2012d6949ce478b8c8c Mon Sep 17 00:00:00 2001 From: Franck Cuny Date: Sat, 9 Oct 2021 18:16:15 -0700 Subject: scrobbler: initial log record type Define the type for a log record and add an helper function to create a new record. --- go.mod | 5 ++++- go.sum | 2 ++ internal/scrobbler/record.go | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 internal/scrobbler/record.go diff --git a/go.mod b/go.mod index cafa7bc..6ffe974 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module golang.fcuny.net/mpd-stats go 1.17 -require github.com/fhs/gompd/v2 v2.2.0 +require ( + github.com/fhs/gompd/v2 v2.2.0 + github.com/google/uuid v1.3.0 +) diff --git a/go.sum b/go.sum index 1c3cda7..127090d 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ github.com/fhs/gompd/v2 v2.2.0 h1:zdSYAAOzQ5cCCgYa5CoXkL0Vr0Cqb/b5JmTobirLc90= github.com/fhs/gompd/v2 v2.2.0/go.mod h1:nNdZtcpD5VpmzZbRl5rV6RhxeMmAWTxEsSIMBkmMIy4= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/internal/scrobbler/record.go b/internal/scrobbler/record.go new file mode 100644 index 0000000..927ed27 --- /dev/null +++ b/internal/scrobbler/record.go @@ -0,0 +1,34 @@ +package scrobbler + +import ( + "strconv" + "time" + + "github.com/fhs/gompd/v2/mpd" + "github.com/google/uuid" +) + +type Record struct { + Id uuid.UUID + Title string + Album string + Artist string + Duration time.Duration +} + +func NewRecord(attrs mpd.Attrs) (*Record, error) { + record := Record{ + Id: uuid.New(), + Title: attrs["Title"], + Album: attrs["Album"], + Artist: attrs["Artist"], + } + + dur, err := strconv.ParseFloat(attrs["duration"], 32) + if err != nil { + return nil, err + } + + record.Duration = time.Second * time.Duration(dur) + return &record, nil +} -- cgit v1.2.3 From 6fde72a495edef3ed108fec5a3324638a8518361 Mon Sep 17 00:00:00 2001 From: Franck Cuny Date: Sat, 9 Oct 2021 18:19:17 -0700 Subject: scrobbler: create a record on new song When we receive an event from the player, we look if the song is different from the previous one, and we create a new record if that's the case. If the song is similar, there's nothing to do. --- cmd/mpd-scrobbler/main.go | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/cmd/mpd-scrobbler/main.go b/cmd/mpd-scrobbler/main.go index 9929225..40b1348 100644 --- a/cmd/mpd-scrobbler/main.go +++ b/cmd/mpd-scrobbler/main.go @@ -4,6 +4,7 @@ import ( "log" "golang.fcuny.net/mpd-stats/internal/mpd" + "golang.fcuny.net/mpd-stats/internal/scrobbler" ) func main() { @@ -18,6 +19,10 @@ func main() { defer c.Watcher.Close() defer c.Client.Close() + var ( + currentRecord *scrobbler.Record + previousRecord *scrobbler.Record + ) for { e := <-c.Watcher.Event if e != "" { @@ -25,11 +30,28 @@ func main() { if err != nil { log.Fatalf("could not get current song: %v", err) } - currentAlbum := attrs["Album"] - artist := attrs["Artist"] - song := attrs["Title"] - duration := attrs["duration"] - log.Printf("we're playing %s/%s/%s [%s]\n", artist, currentAlbum, song, duration) + + if currentRecord == nil { + currentRecord, err = scrobbler.NewRecord(attrs) + if err != nil { + log.Fatalf("could not create a log: %v", err) + } + log.Printf("we're playing %s/%s/%s [%s]\n", currentRecord.Artist, currentRecord.Album, currentRecord.Title, currentRecord.Duration) + previousRecord = currentRecord + continue + } + + if currentRecord.Title != attrs["Title"] || currentRecord.Artist != attrs["Artist"] || currentRecord.Album != attrs["Album"] { + currentRecord, err = scrobbler.NewRecord(attrs) + if err != nil { + log.Fatalf("could not create a log: %v", err) + } + } + + if currentRecord.Id != previousRecord.Id { + log.Printf("we're playing %s/%s/%s [%s]\n", currentRecord.Artist, currentRecord.Album, currentRecord.Title, currentRecord.Duration) + previousRecord = currentRecord + } } } } -- cgit v1.2.3 From 14e16c0b3818a68cf3a1f26f9cd7461481d00e4d Mon Sep 17 00:00:00 2001 From: Franck Cuny Date: Sun, 10 Oct 2021 11:32:10 -0700 Subject: mpd: rename function to create the player --- cmd/mpd-scrobbler/main.go | 2 +- internal/mpd/mpd.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/mpd-scrobbler/main.go b/cmd/mpd-scrobbler/main.go index 40b1348..3953123 100644 --- a/cmd/mpd-scrobbler/main.go +++ b/cmd/mpd-scrobbler/main.go @@ -11,7 +11,7 @@ func main() { net := "tcp" addr := "localhost:6600" - c, err := mpd.NewMPD(net, addr) + c, err := mpd.NewPlayer(net, addr) if err != nil { log.Fatalf("failed to create a client: %v", err) } diff --git a/internal/mpd/mpd.go b/internal/mpd/mpd.go index 8991adc..ed9d3c2 100644 --- a/internal/mpd/mpd.go +++ b/internal/mpd/mpd.go @@ -12,7 +12,7 @@ type player struct { Client *mpd.Client } -func NewMPD(net string, addr string) (*player, error) { +func NewPlayer(net string, addr string) (*player, error) { var ( p player err error -- cgit v1.2.3 From 5caeabc351feffb1769b842e8d79481f330c46d9 Mon Sep 17 00:00:00 2001 From: Franck Cuny Date: Sun, 10 Oct 2021 11:34:07 -0700 Subject: mpd: add function `Close` to the player Let's close both the watcher and the client, instead of leaking this interface to the user. --- cmd/mpd-scrobbler/main.go | 7 +++++-- internal/mpd/mpd.go | 10 ++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/cmd/mpd-scrobbler/main.go b/cmd/mpd-scrobbler/main.go index 3953123..ba7bb05 100644 --- a/cmd/mpd-scrobbler/main.go +++ b/cmd/mpd-scrobbler/main.go @@ -16,8 +16,11 @@ func main() { log.Fatalf("failed to create a client: %v", err) } - defer c.Watcher.Close() - defer c.Client.Close() + defer func() { + if err := c.Close(); err != nil { + log.Fatalf("failed to close the player: %v", err) + } + }() var ( currentRecord *scrobbler.Record diff --git a/internal/mpd/mpd.go b/internal/mpd/mpd.go index ed9d3c2..cebf2b2 100644 --- a/internal/mpd/mpd.go +++ b/internal/mpd/mpd.go @@ -36,3 +36,13 @@ func NewPlayer(net string, addr string) (*player, error) { return &p, nil } + +func (p *player) Close() error { + if err := p.Watcher.Close(); err != nil { + return err + } + if err := p.Client.Close(); err != nil { + return err + } + return nil +} -- cgit v1.2.3 From 7076c83408f4525140718d1b6cc7c90ae828b8f2 Mon Sep 17 00:00:00 2001 From: Franck Cuny Date: Sun, 10 Oct 2021 11:37:19 -0700 Subject: mpd: export the type Player --- internal/mpd/mpd.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/mpd/mpd.go b/internal/mpd/mpd.go index cebf2b2..5b57ee4 100644 --- a/internal/mpd/mpd.go +++ b/internal/mpd/mpd.go @@ -7,14 +7,14 @@ import ( "github.com/fhs/gompd/v2/mpd" ) -type player struct { +type Player struct { Watcher *mpd.Watcher Client *mpd.Client } -func NewPlayer(net string, addr string) (*player, error) { +func NewPlayer(net string, addr string) (*Player, error) { var ( - p player + p Player err error ) @@ -37,7 +37,7 @@ func NewPlayer(net string, addr string) (*player, error) { return &p, nil } -func (p *player) Close() error { +func (p *Player) Close() error { if err := p.Watcher.Close(); err != nil { return err } -- cgit v1.2.3 From 6440b7f28190eff567fa411ead2adbd80d2d870e Mon Sep 17 00:00:00 2001 From: Franck Cuny Date: Sun, 10 Oct 2021 11:44:47 -0700 Subject: scrobbler: add functions to create and run it Add a new function to create a scrobbler. The function takes care of creating the mpd client. Add a function to run the scrobbler, which takes care of creating a new record when needed. This will simplify the interface for the caller, as all they really care about is: create the scrobbler, close it when we're done, and collect songs information while we listen to our music. --- internal/scrobbler/scrobbler.go | 66 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 internal/scrobbler/scrobbler.go diff --git a/internal/scrobbler/scrobbler.go b/internal/scrobbler/scrobbler.go new file mode 100644 index 0000000..061b909 --- /dev/null +++ b/internal/scrobbler/scrobbler.go @@ -0,0 +1,66 @@ +package scrobbler + +import ( + "log" + + "golang.fcuny.net/mpd-stats/internal/mpd" +) + +type Scrobbler struct { + player *mpd.Player +} + +func NewScrobbler(net string, addr string) (*Scrobbler, error) { + var s Scrobbler + + p, err := mpd.NewPlayer(net, addr) + if err != nil { + return nil, err + } + + s.player = p + return &s, nil +} + +func (s *Scrobbler) Close() error { + return s.player.Close() +} + +func (s *Scrobbler) Run() error { + var ( + currentRecord *Record + previousRecord *Record + ) + + for { + e := <-s.player.Watcher.Event + if e != "" { + attrs, err := s.player.Client.CurrentSong() + if err != nil { + log.Fatalf("could not get current song: %v", err) + } + + if currentRecord == nil { + currentRecord, err = NewRecord(attrs) + if err != nil { + log.Fatalf("could not create a log: %v", err) + } + log.Printf("we're playing %s/%s/%s [%s]\n", currentRecord.Artist, currentRecord.Album, currentRecord.Title, currentRecord.Duration) + previousRecord = currentRecord + continue + } + + if currentRecord.Title != attrs["Title"] || currentRecord.Artist != attrs["Artist"] || currentRecord.Album != attrs["Album"] { + currentRecord, err = NewRecord(attrs) + if err != nil { + log.Fatalf("could not create a log: %v", err) + } + } + + if currentRecord.Id != previousRecord.Id { + log.Printf("we're playing %s/%s/%s [%s]\n", currentRecord.Artist, currentRecord.Album, currentRecord.Title, currentRecord.Duration) + previousRecord = currentRecord + } + } + } +} -- cgit v1.2.3 From b5c00edf16716e4398c5ce0827164648f203e8d6 Mon Sep 17 00:00:00 2001 From: Franck Cuny Date: Sun, 10 Oct 2021 11:47:52 -0700 Subject: mpd-stats: create and run the scrobbler --- cmd/mpd-scrobbler/main.go | 43 ++++--------------------------------------- 1 file changed, 4 insertions(+), 39 deletions(-) diff --git a/cmd/mpd-scrobbler/main.go b/cmd/mpd-scrobbler/main.go index ba7bb05..3540807 100644 --- a/cmd/mpd-scrobbler/main.go +++ b/cmd/mpd-scrobbler/main.go @@ -3,7 +3,6 @@ package main import ( "log" - "golang.fcuny.net/mpd-stats/internal/mpd" "golang.fcuny.net/mpd-stats/internal/scrobbler" ) @@ -11,50 +10,16 @@ func main() { net := "tcp" addr := "localhost:6600" - c, err := mpd.NewPlayer(net, addr) + s, err := scrobbler.NewScrobbler(net, addr) if err != nil { log.Fatalf("failed to create a client: %v", err) } defer func() { - if err := c.Close(); err != nil { - log.Fatalf("failed to close the player: %v", err) + if err := s.Close(); err != nil { + log.Fatalf("failed to close the scrobbler: %v", err) } }() - var ( - currentRecord *scrobbler.Record - previousRecord *scrobbler.Record - ) - for { - e := <-c.Watcher.Event - if e != "" { - attrs, err := c.Client.CurrentSong() - if err != nil { - log.Fatalf("could not get current song: %v", err) - } - - if currentRecord == nil { - currentRecord, err = scrobbler.NewRecord(attrs) - if err != nil { - log.Fatalf("could not create a log: %v", err) - } - log.Printf("we're playing %s/%s/%s [%s]\n", currentRecord.Artist, currentRecord.Album, currentRecord.Title, currentRecord.Duration) - previousRecord = currentRecord - continue - } - - if currentRecord.Title != attrs["Title"] || currentRecord.Artist != attrs["Artist"] || currentRecord.Album != attrs["Album"] { - currentRecord, err = scrobbler.NewRecord(attrs) - if err != nil { - log.Fatalf("could not create a log: %v", err) - } - } - - if currentRecord.Id != previousRecord.Id { - log.Printf("we're playing %s/%s/%s [%s]\n", currentRecord.Artist, currentRecord.Album, currentRecord.Title, currentRecord.Duration) - previousRecord = currentRecord - } - } - } + s.Run() } -- cgit v1.2.3 From 07342c7dd5ac6c959629968af20a82232393d9d9 Mon Sep 17 00:00:00 2001 From: Franck Cuny Date: Sun, 10 Oct 2021 12:58:56 -0700 Subject: scrobbler: add interface to the sqlite3 database We want to persist the records in a database, so we can extract statistics and an history. The module for the database is straightforward: it opens the database if it exists and return an handler to it. If the database does not exists, we create it and we create the only table we need (records). --- go.mod | 1 + go.sum | 2 ++ internal/scrobbler/db.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 internal/scrobbler/db.go diff --git a/go.mod b/go.mod index 6ffe974..cc9971c 100644 --- a/go.mod +++ b/go.mod @@ -5,4 +5,5 @@ go 1.17 require ( github.com/fhs/gompd/v2 v2.2.0 github.com/google/uuid v1.3.0 + github.com/mattn/go-sqlite3 v1.14.8 ) diff --git a/go.sum b/go.sum index 127090d..fab0f00 100644 --- a/go.sum +++ b/go.sum @@ -2,3 +2,5 @@ github.com/fhs/gompd/v2 v2.2.0 h1:zdSYAAOzQ5cCCgYa5CoXkL0Vr0Cqb/b5JmTobirLc90= github.com/fhs/gompd/v2 v2.2.0/go.mod h1:nNdZtcpD5VpmzZbRl5rV6RhxeMmAWTxEsSIMBkmMIy4= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU= +github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= diff --git a/internal/scrobbler/db.go b/internal/scrobbler/db.go new file mode 100644 index 0000000..a788b4c --- /dev/null +++ b/internal/scrobbler/db.go @@ -0,0 +1,54 @@ +package scrobbler + +import ( + "database/sql" + "fmt" + "os" + + _ "github.com/mattn/go-sqlite3" +) + +func initdb(dbpath string) error { + if _, err := os.Stat(dbpath); err == nil { + return fmt.Errorf("%s already exists", dbpath) + } + + db, err := sql.Open("sqlite3", dbpath) + if err != nil { + return err + } + defer db.Close() + + sqlStmt := `create table records (id text primary key, + title text, + artist text, + album text, + duration int, + time timestamp + );` + + _, err = db.Exec(sqlStmt) + if err != nil { + return err + } + + return nil +} + +func opendatabase(dbpath string) (*sql.DB, error) { + var err error + _, err = os.Stat(dbpath) + + if err != nil { + if err := initdb(dbpath); err != nil { + return nil, err + } + } + + db, err := sql.Open("sqlite3", dbpath) + if err != nil { + return nil, fmt.Errorf("unable to open database: %s", err) + } + + return db, nil +} -- cgit v1.2.3 From ff915c334278d0e4d8acd8698f375022979a2c96 Mon Sep 17 00:00:00 2001 From: Franck Cuny Date: Sun, 10 Oct 2021 13:00:38 -0700 Subject: scrobbler: add timestamp to the record When we create a new record, let's capture when this was created. --- internal/scrobbler/record.go | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/internal/scrobbler/record.go b/internal/scrobbler/record.go index 927ed27..b9f95a0 100644 --- a/internal/scrobbler/record.go +++ b/internal/scrobbler/record.go @@ -9,19 +9,21 @@ import ( ) type Record struct { - Id uuid.UUID - Title string - Album string - Artist string - Duration time.Duration + Id uuid.UUID + Title string + Album string + Artist string + Duration time.Duration + Timestamp time.Time } func NewRecord(attrs mpd.Attrs) (*Record, error) { record := Record{ - Id: uuid.New(), - Title: attrs["Title"], - Album: attrs["Album"], - Artist: attrs["Artist"], + Id: uuid.New(), + Title: attrs["Title"], + Album: attrs["Album"], + Artist: attrs["Artist"], + Timestamp: time.Now(), } dur, err := strconv.ParseFloat(attrs["duration"], 32) -- cgit v1.2.3 From c95f72a953e6a9068c1e7b89f530fa05f10c4bde Mon Sep 17 00:00:00 2001 From: Franck Cuny Date: Sun, 10 Oct 2021 13:01:21 -0700 Subject: mpd-stats: pass database path to the scrobbler When creating a scrobbler, we provide the path to the database. The scrobbler then get a handler to the database. When a new record is created, we persist it to the database using the `save` function. --- cmd/mpd-scrobbler/main.go | 5 ++++- internal/scrobbler/scrobbler.go | 31 +++++++++++++++++++++++++++---- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/cmd/mpd-scrobbler/main.go b/cmd/mpd-scrobbler/main.go index 3540807..fdd9e4d 100644 --- a/cmd/mpd-scrobbler/main.go +++ b/cmd/mpd-scrobbler/main.go @@ -1,7 +1,9 @@ package main import ( + "fmt" "log" + "os" "golang.fcuny.net/mpd-stats/internal/scrobbler" ) @@ -9,8 +11,9 @@ import ( func main() { net := "tcp" addr := "localhost:6600" + dbpath := fmt.Sprintf("%s/.config/scrobbler.sql", os.Getenv("HOME")) - s, err := scrobbler.NewScrobbler(net, addr) + s, err := scrobbler.NewScrobbler(net, addr, dbpath) if err != nil { log.Fatalf("failed to create a client: %v", err) } diff --git a/internal/scrobbler/scrobbler.go b/internal/scrobbler/scrobbler.go index 061b909..a31569e 100644 --- a/internal/scrobbler/scrobbler.go +++ b/internal/scrobbler/scrobbler.go @@ -1,6 +1,7 @@ package scrobbler import ( + "database/sql" "log" "golang.fcuny.net/mpd-stats/internal/mpd" @@ -8,17 +9,25 @@ import ( type Scrobbler struct { player *mpd.Player + db *sql.DB } -func NewScrobbler(net string, addr string) (*Scrobbler, error) { - var s Scrobbler - +func NewScrobbler(net string, addr string, dbpath string) (*Scrobbler, error) { p, err := mpd.NewPlayer(net, addr) if err != nil { return nil, err } - s.player = p + db, err := opendatabase(dbpath) + if err != nil { + return nil, err + } + + s := Scrobbler{ + player: p, + db: db, + } + return &s, nil } @@ -47,6 +56,7 @@ func (s *Scrobbler) Run() error { } log.Printf("we're playing %s/%s/%s [%s]\n", currentRecord.Artist, currentRecord.Album, currentRecord.Title, currentRecord.Duration) previousRecord = currentRecord + s.save(currentRecord) continue } @@ -60,7 +70,20 @@ func (s *Scrobbler) Run() error { if currentRecord.Id != previousRecord.Id { log.Printf("we're playing %s/%s/%s [%s]\n", currentRecord.Artist, currentRecord.Album, currentRecord.Title, currentRecord.Duration) previousRecord = currentRecord + s.save(currentRecord) } } } } + +func (s *Scrobbler) save(record *Record) error { + _, err := s.db.Exec("insert into records(id, title, artist, album, duration, time) values(?, ?, ?, ?, ?, ?)", + record.Id, + record.Title, + record.Artist, + record.Album, + int(record.Duration.Seconds()), + record.Timestamp, + ) + return err +} -- cgit v1.2.3 From e86984a78aa0c95bca878c8d24f143b1e515a3f0 Mon Sep 17 00:00:00 2001 From: Franck Cuny Date: Sun, 10 Oct 2021 13:22:14 -0700 Subject: record: add some basic testing --- internal/scrobbler/record.go | 6 +++++ internal/scrobbler/record_test.go | 53 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 internal/scrobbler/record_test.go diff --git a/internal/scrobbler/record.go b/internal/scrobbler/record.go index b9f95a0..e252fd3 100644 --- a/internal/scrobbler/record.go +++ b/internal/scrobbler/record.go @@ -34,3 +34,9 @@ func NewRecord(attrs mpd.Attrs) (*Record, error) { record.Duration = time.Second * time.Duration(dur) return &record, nil } + +func (r *Record) EqualAttrs(attrs mpd.Attrs) bool { + return r.Title == attrs["Title"] && + r.Album == attrs["Album"] && + r.Artist == attrs["Artist"] +} diff --git a/internal/scrobbler/record_test.go b/internal/scrobbler/record_test.go new file mode 100644 index 0000000..3bf8554 --- /dev/null +++ b/internal/scrobbler/record_test.go @@ -0,0 +1,53 @@ +package scrobbler + +import ( + "testing" + + "github.com/fhs/gompd/v2/mpd" +) + +func TestNewRecord(t *testing.T) { + song := mpd.Attrs{ + "Artist": "Nine Inch Nails", + "Album": "The Downward Spiral", + "Title": "Reptile", + "duration": "411.00", + } + + record, err := NewRecord(song) + if err != nil { + t.Errorf("NewRecord returned an error: %s", err) + } + if record == nil { + t.Errorf("NewRecord returned nil record") + } +} + +func TestRecordEqualAttrs(t *testing.T) { + s1 := mpd.Attrs{ + "Artist": "Nine Inch Nails", + "Album": "The Downward Spiral", + "Title": "Reptile", + "duration": "411.00", + } + + s2 := mpd.Attrs{ + "Artist": "Nine Inch Nails", + "Album": "The Downward Spiral", + "Title": "Closer", + "duration": "373.00", + } + + r, err := NewRecord(s1) + if err != nil { + t.Errorf("NewRecord returned an error: %s", err) + } + + if !r.EqualAttrs(s1) { + t.Errorf("EqualAttrs expected true got false") + } + + if r.EqualAttrs(s2) { + t.Errorf("EqualAttrs expected false got true") + } +} -- cgit v1.2.3 From e8059bd9a8ea7c1bba29ca947c7c7fa7eedee8cb Mon Sep 17 00:00:00 2001 From: Franck Cuny Date: Sun, 10 Oct 2021 13:24:29 -0700 Subject: scrobbler: use helper function EqualAttrs To compare the current attributes with the current record, we can use the helper `EqualAttrs` which will tell us if we need to create a new record or not. --- internal/scrobbler/scrobbler.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/internal/scrobbler/scrobbler.go b/internal/scrobbler/scrobbler.go index a31569e..e16458c 100644 --- a/internal/scrobbler/scrobbler.go +++ b/internal/scrobbler/scrobbler.go @@ -54,13 +54,12 @@ func (s *Scrobbler) Run() error { if err != nil { log.Fatalf("could not create a log: %v", err) } - log.Printf("we're playing %s/%s/%s [%s]\n", currentRecord.Artist, currentRecord.Album, currentRecord.Title, currentRecord.Duration) previousRecord = currentRecord s.save(currentRecord) continue } - if currentRecord.Title != attrs["Title"] || currentRecord.Artist != attrs["Artist"] || currentRecord.Album != attrs["Album"] { + if !currentRecord.EqualAttrs(attrs) { currentRecord, err = NewRecord(attrs) if err != nil { log.Fatalf("could not create a log: %v", err) @@ -68,7 +67,6 @@ func (s *Scrobbler) Run() error { } if currentRecord.Id != previousRecord.Id { - log.Printf("we're playing %s/%s/%s [%s]\n", currentRecord.Artist, currentRecord.Album, currentRecord.Title, currentRecord.Duration) previousRecord = currentRecord s.save(currentRecord) } -- cgit v1.2.3 From 8ce42165d8baa3b5e8e1feac072ca8275bfcb826 Mon Sep 17 00:00:00 2001 From: Franck Cuny Date: Sun, 10 Oct 2021 16:10:32 -0700 Subject: mpd-scrobbler: proper default arguments The program needs two arguments: the mpd host and port, which can be passed as flags (default is to use the local instance of mpd). We store the database in `XDG_CONFIG_HOME/mpd-scrobbler`, and we create the path if needed. --- cmd/mpd-scrobbler/main.go | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/cmd/mpd-scrobbler/main.go b/cmd/mpd-scrobbler/main.go index fdd9e4d..c2693a4 100644 --- a/cmd/mpd-scrobbler/main.go +++ b/cmd/mpd-scrobbler/main.go @@ -1,17 +1,29 @@ package main import ( + "flag" "fmt" "log" "os" + "path/filepath" "golang.fcuny.net/mpd-stats/internal/scrobbler" ) func main() { + var ( + mpdHost = flag.String("host", "localhost", "The MPD server to connect to (default: localhost)") + mpdPort = flag.Int("port", 6600, "The TCP port of the MPD server to connect to (default: 6600)") + ) + flag.Parse() + net := "tcp" - addr := "localhost:6600" - dbpath := fmt.Sprintf("%s/.config/scrobbler.sql", os.Getenv("HOME")) + addr := fmt.Sprintf("%s:%d", *mpdHost, *mpdPort) + + dbpath, err := getDbPath() + if err != nil { + log.Fatalf("failed to get the path to the database: %v", err) + } s, err := scrobbler.NewScrobbler(net, addr, dbpath) if err != nil { @@ -26,3 +38,20 @@ func main() { s.Run() } + +func getDbPath() (string, error) { + xch := os.Getenv("XDG_CONFIG_HOME") + if xch == "" { + home := os.Getenv("HOME") + xch = filepath.Join(home, ".config") + } + + scrobblerHome := filepath.Join(xch, "mpd-scrobbler") + if _, err := os.Stat(scrobblerHome); os.IsNotExist(err) { + if err := os.Mkdir(scrobblerHome, 0755); err != nil { + return "", err + } + } + + return filepath.Join(scrobblerHome, "scrobbler.sql"), nil +} -- cgit v1.2.3 From a4fe1e3ef4e06e19f146a0a6498cdb550ef8b4f3 Mon Sep 17 00:00:00 2001 From: Franck Cuny Date: Sun, 10 Oct 2021 17:52:19 -0700 Subject: scrobbler: record how long a song was played Add a column `playtime` to the records table to keep track of how long a song was played. With this information, in the future, we will be able to sum up how long we listen to music, but also which songs were skipped. --- internal/scrobbler/db.go | 1 + internal/scrobbler/scrobbler.go | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/internal/scrobbler/db.go b/internal/scrobbler/db.go index a788b4c..5f80aa4 100644 --- a/internal/scrobbler/db.go +++ b/internal/scrobbler/db.go @@ -24,6 +24,7 @@ func initdb(dbpath string) error { artist text, album text, duration int, + playtime int, time timestamp );` diff --git a/internal/scrobbler/scrobbler.go b/internal/scrobbler/scrobbler.go index e16458c..df8e46a 100644 --- a/internal/scrobbler/scrobbler.go +++ b/internal/scrobbler/scrobbler.go @@ -3,6 +3,7 @@ package scrobbler import ( "database/sql" "log" + "time" "golang.fcuny.net/mpd-stats/internal/mpd" ) @@ -67,6 +68,9 @@ func (s *Scrobbler) Run() error { } if currentRecord.Id != previousRecord.Id { + if err := s.update(previousRecord); err != nil { + log.Printf("failed to update record %s: %s", previousRecord.Id, err) + } previousRecord = currentRecord s.save(currentRecord) } @@ -75,7 +79,7 @@ func (s *Scrobbler) Run() error { } func (s *Scrobbler) save(record *Record) error { - _, err := s.db.Exec("insert into records(id, title, artist, album, duration, time) values(?, ?, ?, ?, ?, ?)", + _, err := s.db.Exec("insert into records(id, title, artist, album, duration, playtime, time) values(?, ?, ?, ?, ?, 0, ?)", record.Id, record.Title, record.Artist, @@ -85,3 +89,13 @@ func (s *Scrobbler) save(record *Record) error { ) return err } + +func (s *Scrobbler) update(record *Record) error { + tnow := time.Now() + playtime := tnow.Sub(record.Timestamp).Seconds() + _, err := s.db.Exec("update records set playtime = ? where id = ?", + int(playtime), + record.Id, + ) + return err +} -- cgit v1.2.3 From ed67fc99451452b7df32f6609f8e9798089e243f Mon Sep 17 00:00:00 2001 From: Franck Cuny Date: Mon, 11 Oct 2021 19:34:59 -0700 Subject: scrobbler: read mpd status before processing song If the status of the player is "stop", we don't have a new song to handle. In this case, if there's a current song, let's update the status and clear our state. Closes #1. --- internal/scrobbler/scrobbler.go | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/internal/scrobbler/scrobbler.go b/internal/scrobbler/scrobbler.go index df8e46a..f0f9d0e 100644 --- a/internal/scrobbler/scrobbler.go +++ b/internal/scrobbler/scrobbler.go @@ -45,25 +45,42 @@ func (s *Scrobbler) Run() error { for { e := <-s.player.Watcher.Event if e != "" { + status, err := s.player.Client.Status() + if err != nil { + log.Printf("could not read the status: %v", err) + } + + if status["state"] == "stop" { + if currentRecord != nil { + if err := s.update(currentRecord); err != nil { + log.Printf("failed to update record %s: %s", currentRecord.Id, err) + } + currentRecord = nil + } + continue + } + attrs, err := s.player.Client.CurrentSong() if err != nil { - log.Fatalf("could not get current song: %v", err) + log.Printf("could not get current song: %v", err) } if currentRecord == nil { currentRecord, err = NewRecord(attrs) if err != nil { - log.Fatalf("could not create a log: %v", err) + log.Printf("could not create a log: %v", err) } previousRecord = currentRecord - s.save(currentRecord) + if err := s.save(currentRecord); err != nil { + log.Printf("failed to insert record %s: %s", currentRecord.Id, err) + } continue } if !currentRecord.EqualAttrs(attrs) { currentRecord, err = NewRecord(attrs) if err != nil { - log.Fatalf("could not create a log: %v", err) + log.Printf("could not create a log: %v", err) } } -- cgit v1.2.3 From 2c1a4d083b1a0a01c2d552123920783fafa509ca Mon Sep 17 00:00:00 2001 From: Franck Cuny Date: Mon, 25 Oct 2021 10:10:42 -0700 Subject: scrobbler: add a systemd unit file As I want the scrobbler to be started automatically when I log into my session, the easiest way to do this is by having a systemd unit file that I run for my own user. The unit expects that the binary for the scrobbler is under my $GOPATH, which is hard coded for now. We also ensure that the binary exists before starting the unit. We harness the service with a number of directives. --- systemd/mpd-scrobbler.service | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 systemd/mpd-scrobbler.service diff --git a/systemd/mpd-scrobbler.service b/systemd/mpd-scrobbler.service new file mode 100644 index 0000000..7990208 --- /dev/null +++ b/systemd/mpd-scrobbler.service @@ -0,0 +1,43 @@ +[Unit] +Description=mpd scrobbler +Documentation=https://git.fcuny.net/fcuny/mpd-stats +ConditionFileIsExecutable=%h/workspace/go/bin/mpd-scrobbler + +[Service] +ExecStart=%h/workspace/go/bin/mpd-scrobbler +Restart=on-failure + +PrivateTmp=yes +ProtectSystem=strict +NoNewPrivileges=yes +ProtectHome=yes + +# Prohibit access to any kind of namespacing: +RestrictNamespaces=yes + +# Make cgroup file system hierarchy inaccessible: +ProtectControlGroups=yes + +# Deny access to other user’s information in /proc: +ProtectProc=invisible + +# Only allow access to /proc pid files, no other files: +ProcSubset=pid + +# This daemon must not create any new files, but set the umask to 077 just in case. +UMask=077 + +# Filter dangerous system calls. The following is listed as safe basic choice +# in systemd.exec(5): +SystemCallArchitectures=native +SystemCallFilter=@system-service +SystemCallFilter=~@privileged +SystemCallFilter=~@resources +SystemCallErrorNumber=EPERM + +# Deny kernel execution domain changing: +LockPersonality=yes + +# Deny memory mappings that are writable and executable: +MemoryDenyWriteExecute=yes + -- cgit v1.2.3 From 82577799559edae983f59695b4a9af921b2e2c9c Mon Sep 17 00:00:00 2001 From: Franck Cuny Date: Mon, 25 Oct 2021 10:24:43 -0700 Subject: build: install the binary and systemd unit Add a Makefile to install the binary in GOPATH and the unit file for the service. When the binary is installed, systemd is reloaded and the service is also started. --- Makefile | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0edb9dd --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +GO_INSTALL_ARGS := -trimpath + +.PHONY: install + +install: + @go install $(GO_INSTALL_ARGS) ./cmd/mpd-scrobbler + @install -m 0644 systemd/mpd-scrobbler.service $(HOME)/.config/systemd/user/ + @echo "reloading systemd" + @systemctl --user daemon-reload + @echo "starting the unit" + @systemctl --user restart mpd-scrobbler + @systemctl --user status mpd-scrobbler -- cgit v1.2.3 From de9465cbf8bc9afd5e4452dfe79809b173418b11 Mon Sep 17 00:00:00 2001 From: Franck Cuny Date: Mon, 25 Oct 2021 10:27:21 -0700 Subject: doc: update README with instruction for installing --- README.org | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.org b/README.org index 8765e59..8c0a7d9 100644 --- a/README.org +++ b/README.org @@ -12,3 +12,11 @@ Collect logs from mpd. A log record is composed of the following fields: - date: date the song was played The logs are recorded in a database (sqlite3 to start). +* Install +The Makefile assumes the system is running Linux and systemd. + +Run =make install=. This will: +- install the binary in your =GOPATH= (using =go install=) +- install a systemd unit file under =$HOME/.config/systemd/user= +- reload systemd unit files +- start the service -- cgit v1.2.3 From d93163f2c28bef5b4a1656ed1cf6305c0eaec067 Mon Sep 17 00:00:00 2001 From: Franck Cuny Date: Fri, 29 Aug 2025 09:22:36 -0700 Subject: prepare the migration --- LICENSE.txt | 20 -------------------- Makefile | 12 ------------ README.org | 22 ---------------------- cmd/mpd-scrobbler/README.org | 22 ++++++++++++++++++++++ go.mod | 9 --------- go.sum | 6 ------ 6 files changed, 22 insertions(+), 69 deletions(-) delete mode 100644 LICENSE.txt delete mode 100644 Makefile delete mode 100644 README.org create mode 100644 cmd/mpd-scrobbler/README.org delete mode 100644 go.mod delete mode 100644 go.sum diff --git a/LICENSE.txt b/LICENSE.txt deleted file mode 100644 index e614d87..0000000 --- a/LICENSE.txt +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (c) 2021 Franck Cuny - -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. diff --git a/Makefile b/Makefile deleted file mode 100644 index 0edb9dd..0000000 --- a/Makefile +++ /dev/null @@ -1,12 +0,0 @@ -GO_INSTALL_ARGS := -trimpath - -.PHONY: install - -install: - @go install $(GO_INSTALL_ARGS) ./cmd/mpd-scrobbler - @install -m 0644 systemd/mpd-scrobbler.service $(HOME)/.config/systemd/user/ - @echo "reloading systemd" - @systemctl --user daemon-reload - @echo "starting the unit" - @systemctl --user restart mpd-scrobbler - @systemctl --user status mpd-scrobbler diff --git a/README.org b/README.org deleted file mode 100644 index 8c0a7d9..0000000 --- a/README.org +++ /dev/null @@ -1,22 +0,0 @@ -#+TITLE: mpd-stats - -Log played songs to extract statistics. This is similar to what libre.fm used to do, but locally. - -* Logging -Collect logs from mpd. A log record is composed of the following fields: -- id: UUID -- song's name: the name of the song -- song's album: the name of the album -- song's artist: the name of the artist -- song's duration: the duration of the song -- date: date the song was played - -The logs are recorded in a database (sqlite3 to start). -* Install -The Makefile assumes the system is running Linux and systemd. - -Run =make install=. This will: -- install the binary in your =GOPATH= (using =go install=) -- install a systemd unit file under =$HOME/.config/systemd/user= -- reload systemd unit files -- start the service diff --git a/cmd/mpd-scrobbler/README.org b/cmd/mpd-scrobbler/README.org new file mode 100644 index 0000000..8c0a7d9 --- /dev/null +++ b/cmd/mpd-scrobbler/README.org @@ -0,0 +1,22 @@ +#+TITLE: mpd-stats + +Log played songs to extract statistics. This is similar to what libre.fm used to do, but locally. + +* Logging +Collect logs from mpd. A log record is composed of the following fields: +- id: UUID +- song's name: the name of the song +- song's album: the name of the album +- song's artist: the name of the artist +- song's duration: the duration of the song +- date: date the song was played + +The logs are recorded in a database (sqlite3 to start). +* Install +The Makefile assumes the system is running Linux and systemd. + +Run =make install=. This will: +- install the binary in your =GOPATH= (using =go install=) +- install a systemd unit file under =$HOME/.config/systemd/user= +- reload systemd unit files +- start the service diff --git a/go.mod b/go.mod deleted file mode 100644 index cc9971c..0000000 --- a/go.mod +++ /dev/null @@ -1,9 +0,0 @@ -module golang.fcuny.net/mpd-stats - -go 1.17 - -require ( - github.com/fhs/gompd/v2 v2.2.0 - github.com/google/uuid v1.3.0 - github.com/mattn/go-sqlite3 v1.14.8 -) diff --git a/go.sum b/go.sum deleted file mode 100644 index fab0f00..0000000 --- a/go.sum +++ /dev/null @@ -1,6 +0,0 @@ -github.com/fhs/gompd/v2 v2.2.0 h1:zdSYAAOzQ5cCCgYa5CoXkL0Vr0Cqb/b5JmTobirLc90= -github.com/fhs/gompd/v2 v2.2.0/go.mod h1:nNdZtcpD5VpmzZbRl5rV6RhxeMmAWTxEsSIMBkmMIy4= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU= -github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -- cgit v1.2.3