diff --git a/README.md b/README.md index 1cb8f3a..3d63f03 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Music library and metadata database. ## Dependencies -### Go >= 1.17 +### Go >= 1.18 ### [Hibiki](https://git.gammaspectra.live/S.O.N.G/Hibiki) dependencies Hibiki is an implementation of Panako in Go, and other audio utilities. diff --git a/database/album.go b/database/album.go new file mode 100644 index 0000000..3790c4e --- /dev/null +++ b/database/album.go @@ -0,0 +1,210 @@ +package database + +import ( + "database/sql" + "encoding/json" + "github.com/lib/pq" +) + +type Album struct { + Type + id int + cover int64 + identifiers []string + metadata []byte +} + +func InsertAlbumToDatabase(db *Database, names map[string]string) *Album { + + //TODO: do it in single tx + + rows, err := db.Query("INSERT INTO albums DEFAULT VALUES RETURNING id;") + if err != nil { + return nil + } + defer rows.Close() + rows.Next() + var id int + err = rows.Scan(&id) + if err != nil { + return nil + } + album := GetAlbumFromDatabase(db, id) + if album == nil { + return nil + } + + for k, n := range names { + album.AddName(k, n) + } + + return album +} + +func GetAlbumsFromDatabaseByName(db *Database, name string) (albums []*Album) { + rows, err := db.Query("SELECT id, cover, identifiers, metadata FROM albums WHERE id IN(SELECT album FROM albums_names WHERE name ILIKE $1);", name) + if err != nil { + return nil + } + defer rows.Close() + for rows.Next() { + album, err := GetAlbumFromRow(db, rows) + if err != nil { + break + } + albums = append(albums, album) + } + return +} + +func GetAlbumFromDatabase(db *Database, id int) *Album { + rows, err := db.Query("SELECT id, cover, identifiers, metadata FROM albums WHERE id = $1;", id) + if err != nil { + return nil + } + defer rows.Close() + rows.Next() + r, err := GetAlbumFromRow(db, rows) + if err != nil { + return nil + } + return r +} + +func GetAlbumFromRow(db *Database, row *sql.Rows) (*Album, error) { + r := &Album{ + Type: Type{ + db: db, + }, + } + var cover sql.NullInt64 + err := row.Scan(&r.id, &cover, pq.Array(&r.identifiers), &r.metadata) + + if cover.Valid { + r.cover = cover.Int64 + } + + if err != nil { + return nil, err + } + return r, nil +} + +func (r *Album) GetId() int { + return r.id +} + +func (r *Album) GetCover() *Resource { + if r.cover == 0 { + return nil + } + + return GetResourceFromDatabase(r.db, r.cover) +} + +func (r *Album) SetCover(resource *Resource) { + if resource == nil { + r.db.Exec("UPDATE albums SET cover = NULL WHERE id = $1;", r.id) + return + } + + r.db.Exec("UPDATE albums SET cover = $2 WHERE id = $1;", r.id, resource.id) +} + +func (r *Album) GetNames() map[string]string { + + result := make(map[string]string) + + rows, err := r.db.Query("SELECT kind, name FROM albums_names WHERE id $1;", r.id) + if err != nil { + return nil + } + defer rows.Close() + for rows.Next() { + var kind string + var name string + err = rows.Scan(&kind, &name) + if err != nil { + break + } + result[kind] = name + } + + return result +} + +func (r *Album) RemoveName(kind string) { + r.db.Exec("DELETE FROM albums_names WHERE id = $1 AND kind = $2;", r.id, kind) +} + +func (r *Album) AddName(kind, name string) { + r.db.Exec("INSERT INTO albums_names (album, kind, name) VALUES ($1, $2, $3) ON CONFLICT (album, kind) DO UPDATE SET name = $3;", r.id, kind, name) +} + +func (r *Album) GetMetadata() (result map[string]interface{}) { + json.Unmarshal(r.metadata, &result) + return result +} + +func (r *Album) SetMetadata(value map[string]interface{}) { + r.metadata, _ = json.Marshal(value) + r.db.Exec("UPDATE albums SET metadata = $2::jsonb WHERE id = $1;", r.id, r.metadata) +} + +func (r *Album) GetIdentifiers() []string { + return r.identifiers +} + +func (r *Album) SetIdentifiers(identifiers []string) { + r.identifiers = identifiers + + r.db.Exec("UPDATE albums SET identifiers = $2 WHERE id = $1;", r.id, pq.Array(r.identifiers)) +} + +func (r *Album) GetTags() (tags []*Tag) { + rows, err := r.db.Query("SELECT id, name FROM tags WHERE id IN(SELECT tag FROM album_taggings WHERE album = $1);", r.id) + if err != nil { + return nil + } + defer rows.Close() + for rows.Next() { + tag, err := GetTagFromRow(r.db, rows) + if err != nil { + break + } + tags = append(tags, tag) + } + return +} + +func (r *Album) RemoveTag(tag *Tag) { + r.db.Exec("DELETE FROM album_taggings WHERE album = $1 AND tag = $2;", r.id, tag.id) +} + +func (r *Album) AddTag(tag *Tag) { + r.db.Exec("INSERT INTO album_taggings (tag, album) VALUES ($2, $1) ON CONFLICT (tag, album) DO NOTHING;", r.id, tag.id) +} + +func (r *Album) GetArtists() (artists []*AlbumArtist) { + rows, err := r.db.Query("SELECT artist as id, (SELECT artists.metadata FROM artists WHERE artists.id = album_artists.artist) as metadata, kind FROM album_artists WHERE song_artists.album = $1;", r.id) + if err != nil { + return nil + } + defer rows.Close() + for rows.Next() { + artist, err := GetAlbumArtistFromRow(r.db, rows) + if err != nil { + break + } + artists = append(artists, artist) + } + return +} + +func (r *Album) RemoveArtist(artist *AlbumArtist) { + r.db.Exec("DELETE FROM album_artists WHERE album = $1 AND kind = $3 AND tag = $2;", r.id, artist.id, artist.kind) +} + +func (r *Album) AddArtist(artist *AlbumArtist) { + r.db.Exec("INSERT INTO album_artists (album, artist, kind) VALUES ($1, $2, $3) ON CONFLICT (album, artist, kind) DO NOTHING;", r.id, artist.id, artist.kind) +} diff --git a/database/artist.go b/database/artist.go index 09dfb90..8c44e78 100644 --- a/database/artist.go +++ b/database/artist.go @@ -6,22 +6,62 @@ import ( ) type Artist struct { - DatabaseType - id int64 + Type + id int metadata []byte } +type SongArtist struct { + Artist + kind string +} + +type AlbumArtist struct { + Artist + kind string +} + +func GetSongArtistFromRow(db *Database, row *sql.Rows) (*SongArtist, error) { + r := &SongArtist{ + Artist: Artist{ + Type: Type{ + db: db, + }, + }, + } + err := row.Scan(&r.id, &r.metadata, &r.kind) + if err != nil { + return nil, err + } + return r, nil +} + +func GetAlbumArtistFromRow(db *Database, row *sql.Rows) (*AlbumArtist, error) { + r := &AlbumArtist{ + Artist: Artist{ + Type: Type{ + db: db, + }, + }, + } + err := row.Scan(&r.id, &r.metadata, &r.kind) + if err != nil { + return nil, err + } + return r, nil +} + func InsertArtistToDatabase(db *Database, names map[string]string) *Artist { //TODO: do it in single tx - rows, err := db.Query("INSERT INTO artist DEFAULT VALUES RETURNING id;") + rows, err := db.Query("INSERT INTO artists DEFAULT VALUES RETURNING id;") if err != nil { return nil } defer rows.Close() rows.Next() - var id int64 + var id int err = rows.Scan(&id) if err != nil { return nil @@ -54,7 +94,7 @@ func GetArtistsFromDatabaseByName(db *Database, name string) (artists []*Artist) return } -func GetArtistFromDatabase(db *Database, id int64) *Artist { +func GetArtistFromDatabase(db *Database, id int) *Artist { rows, err := db.Query("SELECT id, metadata FROM artists WHERE id = $1;", id) if err != nil { return nil @@ -70,7 +110,7 @@ func GetArtistFromDatabase(db *Database, id int64) *Artist { func GetArtistFromRow(db *Database, row *sql.Rows) (*Artist, error) { r := &Artist{ - DatabaseType: DatabaseType{ + Type: Type{ db: db, }, } @@ -81,7 +121,7 @@ func GetArtistFromRow(db *Database, row *sql.Rows) (*Artist, error) { return r, nil } -func (r *Artist) GetId() int64 { +func (r *Artist) GetId() int { return r.id } diff --git a/database/release.go b/database/release.go index 7e03f4c..0d87135 100644 --- a/database/release.go +++ b/database/release.go @@ -7,7 +7,7 @@ import ( ) type Release struct { - DatabaseType + Type id int64 identifiers []string metadata []byte @@ -60,7 +60,7 @@ func GetReleaseFromDatabase(db *Database, id int64) *Release { func GetReleaseFromRow(db *Database, row *sql.Rows) (*Release, error) { r := &Release{ - DatabaseType: DatabaseType{ + Type: Type{ db: db, }, } diff --git a/database/resource.go b/database/resource.go index e935c74..2eb5c21 100644 --- a/database/resource.go +++ b/database/resource.go @@ -12,7 +12,7 @@ func (h ResourceHashIdentifier) ToBytes() []byte { } type Resource struct { - DatabaseType + Type id int64 hash ResourceHashIdentifier size int64 @@ -79,7 +79,7 @@ func GetResourceFromDatabaseByHash(db *Database, hash ResourceHashIdentifier) *R func GetResourceFromRow(db *Database, row *sql.Rows) (*Resource, error) { r := &Resource{ - DatabaseType: DatabaseType{ + Type: Type{ db: db, }, } diff --git a/database/song.go b/database/song.go new file mode 100644 index 0000000..8b0f89f --- /dev/null +++ b/database/song.go @@ -0,0 +1,220 @@ +package database + +import ( + "database/sql" + "encoding/json" +) + +type Song struct { + Type + id int64 + resource int64 + cover int64 + album int + metadata []byte +} + +func InsertSongToDatabase(db *Database, resourceId int64, names map[string]string) *Song { + + //TODO: do it in single tx + + rows, err := db.Query("INSERT INTO songs (resource) VALUES ($1) RETURNING id;", resourceId) + if err != nil { + return nil + } + defer rows.Close() + rows.Next() + var id int64 + err = rows.Scan(&id) + if err != nil { + return nil + } + song := GetSongFromDatabase(db, id) + if song == nil { + return nil + } + + for k, n := range names { + song.AddName(k, n) + } + + return song +} + +func GetSongsFromDatabaseByName(db *Database, name string) (songs []*Song) { + rows, err := db.Query("SELECT id, resource, cover, album, metadata FROM songs WHERE id IN(SELECT song FROM songs_names WHERE name ILIKE $1);", name) + if err != nil { + return nil + } + defer rows.Close() + for rows.Next() { + song, err := GetSongFromRow(db, rows) + if err != nil { + break + } + songs = append(songs, song) + } + return +} + +func GetSongFromDatabase(db *Database, id int64) *Song { + rows, err := db.Query("SELECT id, resource, cover, album, metadata FROM songs WHERE id = $1;", id) + if err != nil { + return nil + } + defer rows.Close() + rows.Next() + r, err := GetSongFromRow(db, rows) + if err != nil { + return nil + } + return r +} + +func GetSongFromRow(db *Database, row *sql.Rows) (*Song, error) { + r := &Song{ + Type: Type{ + db: db, + }, + } + var cover sql.NullInt64 + err := row.Scan(&r.id, &r.resource, &cover, &r.album, &r.metadata) + + if cover.Valid { + r.cover = cover.Int64 + } + + if err != nil { + return nil, err + } + return r, nil +} + +func (r *Song) GetId() int64 { + return r.id +} + +func (r *Song) GetResource() *Resource { + return GetResourceFromDatabase(r.db, r.resource) +} + +func (r *Song) GetCover() *Resource { + if r.cover == 0 { + return nil + } + + return GetResourceFromDatabase(r.db, r.cover) +} + +func (r *Song) SetCover(resource *Resource) { + if resource == nil { + r.db.Exec("UPDATE songs SET cover = NULL WHERE id = $1;", r.id) + return + } + + r.db.Exec("UPDATE songs SET cover = $2 WHERE id = $1;", r.id, resource.id) +} + +func (r *Song) GetAlbum() *Album { + if r.album == 0 { + return nil + } + return GetAlbumFromDatabase(r.db, r.album) +} + +func (r *Song) SetAlbum(album *Album) { + if album == nil { + r.db.Exec("UPDATE songs SET album = NULL WHERE id = $1;", r.id) + return + } + + r.db.Exec("UPDATE songs SET album = $2 WHERE id = $1;", r.id, album.id) +} + +func (r *Song) GetNames() map[string]string { + + result := make(map[string]string) + + rows, err := r.db.Query("SELECT kind, name FROM song_names WHERE id $1;", r.id) + if err != nil { + return nil + } + defer rows.Close() + for rows.Next() { + var kind string + var name string + err = rows.Scan(&kind, &name) + if err != nil { + break + } + result[kind] = name + } + + return result +} + +func (r *Song) RemoveName(kind string) { + r.db.Exec("DELETE FROM song_names WHERE id = $1 AND kind = $2;", r.id, kind) +} + +func (r *Song) AddName(kind, name string) { + r.db.Exec("INSERT INTO song_names (song, kind, name) VALUES ($1, $2, $3) ON CONFLICT (song, kind) DO UPDATE SET name = $3;", r.id, kind, name) +} + +func (r *Song) GetMetadata() (result map[string]interface{}) { + json.Unmarshal(r.metadata, &result) + return result +} + +func (r *Song) SetMetadata(value map[string]interface{}) { + r.metadata, _ = json.Marshal(value) + r.db.Exec("UPDATE songs SET metadata = $2::jsonb WHERE id = $1;", r.id, r.metadata) +} + +func (r *Song) GetTags() (tags []*Tag) { + rows, err := r.db.Query("SELECT id, name FROM tags WHERE id IN(SELECT tag FROM song_taggings WHERE song = $1);", r.id) + if err != nil { + return nil + } + defer rows.Close() + for rows.Next() { + tag, err := GetTagFromRow(r.db, rows) + if err != nil { + break + } + tags = append(tags, tag) + } + return +} + +func (r *Song) RemoveTag(tag *Tag) { + r.db.Exec("DELETE FROM song_taggings WHERE song = $1 AND tag = $2;", r.id, tag.id) +} + +func (r *Song) AddTag(tag *Tag) { + r.db.Exec("INSERT INTO song_taggings (tag, song) VALUES ($2, $1) ON CONFLICT (tag, song) DO NOTHING;", r.id, tag.id) +} + +func (r *Song) GetArtists() (artists []*SongArtist) { + rows, err := r.db.Query("SELECT artist as id, (SELECT artists.metadata FROM artists WHERE artists.id = song_artists.artist) as metadata, kind FROM song_artists WHERE song_artists.song = $1;", r.id) + if err != nil { + return nil + } + defer rows.Close() + for rows.Next() { + artist, err := GetSongArtistFromRow(r.db, rows) + if err != nil { + break + } + artists = append(artists, artist) + } + return +} + +func (r *Song) RemoveArtist(artist *SongArtist) { + r.db.Exec("DELETE FROM song_artists WHERE song = $1 AND kind = $3 AND tag = $2;", r.id, artist.id, artist.kind) +} + +func (r *Song) AddArtist(artist *SongArtist) { + r.db.Exec("INSERT INTO song_artists (song, artist, kind) VALUES ($1, $2, $3) ON CONFLICT (song, artist, kind) DO NOTHING;", r.id, artist.id, artist.kind) +} diff --git a/database/tag.go b/database/tag.go new file mode 100644 index 0000000..ff76986 --- /dev/null +++ b/database/tag.go @@ -0,0 +1,104 @@ +package database + +import ( + "database/sql" +) + +type Tag struct { + Type + id int64 + name string +} + +func InsertTagToDatabase(db *Database, name string) *Tag { + + rows, err := db.Query("INSERT INTO tags (name) VALUES ($1) RETURNING id;", name) + if err != nil { + return nil + } + defer rows.Close() + rows.Next() + var id int64 + err = rows.Scan(&id) + if err != nil { + return nil + } + tag := GetTagFromDatabase(db, id) + + return tag +} + +func GetOrCreateTagFromDatabaseByName(db *Database, name string) (tag *Tag) { + tag = GetTagFromDatabaseByName(db, name) + if tag == nil { + tag = InsertTagToDatabase(db, name) + } + + return +} + +func GetTagFromDatabaseByName(db *Database, name string) *Tag { + rows, err := db.Query("SELECT id, name FROM tags WHERE name ILIKE $1;", name) + if err != nil { + return nil + } + defer rows.Close() + rows.Next() + r, err := GetTagFromRow(db, rows) + if err != nil { + return nil + } + return r +} + +func GetTagFromDatabase(db *Database, id int64) *Tag { + rows, err := db.Query("SELECT id, name FROM tags WHERE id = $1;", id) + if err != nil { + return nil + } + defer rows.Close() + rows.Next() + r, err := GetTagFromRow(db, rows) + if err != nil { + return nil + } + return r +} + +func GetTagFromRow(db *Database, row *sql.Rows) (*Tag, error) { + r := &Tag{ + Type: Type{ + db: db, + }, + } + err := row.Scan(&r.id, &r.name) + + if err != nil { + return nil, err + } + return r, nil +} + +func (r *Tag) GetId() int64 { + return r.id +} + +func (r *Tag) GetName() string { + return r.name +} + +func (r *Tag) GetAlbums() (albums []*Album) { + rows, err := r.db.Query("SELECT id, cover, identifiers, metadata FROM albums WHERE id IN(SELECT album FROM album_taggings WHERE tag = $1);", r.id) + if err != nil { + return nil + } + defer rows.Close() + for rows.Next() { + album, err := GetAlbumFromRow(r.db, rows) + if err != nil { + break + } + albums = append(albums, album) + } + return +} diff --git a/database/type.go b/database/type.go index 1552de0..ed1dbf7 100644 --- a/database/type.go +++ b/database/type.go @@ -1,5 +1,5 @@ package database -type DatabaseType struct { +type Type struct { db *Database }