diff --git a/MyMusicBoxApi/configuration/util.go b/MyMusicBoxApi/configuration/util.go index 0fdc408..250cd94 100644 --- a/MyMusicBoxApi/configuration/util.go +++ b/MyMusicBoxApi/configuration/util.go @@ -3,7 +3,9 @@ package configuration import ( "flag" "fmt" + "musicboxapi/logging" "musicboxapi/models" + "os" ) var Config models.Config @@ -24,5 +26,13 @@ func GetApiGroupUrl(version string) string { } else { return fmt.Sprintf("/api/%s", version) } +} + +func DeleteFile(path string) { + err := os.Remove(path) + if err != nil { + logging.ErrorStackTrace(err) + logging.Error(fmt.Sprintf("Failed to delete file, path: %s", path)) + } } diff --git a/MyMusicBoxApi/database/db.go b/MyMusicBoxApi/database/db.go index 5743ef6..f04fd6b 100644 --- a/MyMusicBoxApi/database/db.go +++ b/MyMusicBoxApi/database/db.go @@ -45,7 +45,7 @@ func CreateDatabasConnectionPool() error { // Should create test for these? var _ ISongTable = (*SongTable)(nil) var _ IPlaylistTable = (*PlaylistTable)(nil) - var _ IPlaylistsongTable = (*PlaylistsongTable)(nil) + var _ IPlaylistSongTable = (*PlaylistsongTable)(nil) var _ ITasklogTable = (*TasklogTable)(nil) var _ IMigrationTable = (*MigrationTable)(nil) @@ -151,8 +151,8 @@ func (base *BaseTable) NonScalarQuery(query string, params ...any) (error error) return nil } -func (base *BaseTable) QueryRow(query string) *sql.Row { - return base.DB.QueryRow(query) +func (base *BaseTable) QueryRow(query string, params ...any) *sql.Row { + return base.DB.QueryRow(query, params...) } func (base *BaseTable) QueryRowsContex(ctx context.Context, query string, params ...any) (*sql.Rows, error) { diff --git a/MyMusicBoxApi/database/playlistsongtable.go b/MyMusicBoxApi/database/playlistsongtable.go index 65cff61..02323e6 100644 --- a/MyMusicBoxApi/database/playlistsongtable.go +++ b/MyMusicBoxApi/database/playlistsongtable.go @@ -7,7 +7,7 @@ import ( "musicboxapi/models" ) -type IPlaylistsongTable interface { +type IPlaylistSongTable interface { FetchPlaylistSongs(ctx context.Context, playlistId int, lastKnowPosition int) (songs []models.Song, error error) InsertPlaylistSong(playlistId int, songId int) (lastInsertedId int, error error) DeleteAllPlaylistSongs(playlistId int) (error error) @@ -18,7 +18,7 @@ type PlaylistsongTable struct { BaseTable } -func NewPlaylistsongTableInstance() IPlaylistsongTable { +func NewPlaylistsongTableInstance() IPlaylistSongTable { return &PlaylistsongTable{ BaseTable: NewBaseTableInstance(), } diff --git a/MyMusicBoxApi/database/playlisttable.go b/MyMusicBoxApi/database/playlisttable.go index 55acb11..12a8e71 100644 --- a/MyMusicBoxApi/database/playlisttable.go +++ b/MyMusicBoxApi/database/playlisttable.go @@ -24,7 +24,7 @@ func NewPlaylistTableInstance() IPlaylistTable { } func (table *PlaylistTable) FetchPlaylists(ctx context.Context, lastKnowPlaylistId int) (playlists []models.Playlist, error error) { - query := "SELECT Id, Name, ThumbnailPath, Description, CreationDate FROM Playlist WHERE Id > $1 ORDER BY Id" // order by? + query := "SELECT Id, Name, ThumbnailPath, Description, CreationDate, IsPublic FROM Playlist WHERE Id > $1 ORDER BY Id" // order by? rows, err := table.QueryRowsContex(ctx, query, lastKnowPlaylistId) @@ -41,7 +41,7 @@ func (table *PlaylistTable) FetchPlaylists(ctx context.Context, lastKnowPlaylist playlists = make([]models.Playlist, 0) for rows.Next() { - scanError := rows.Scan(&playlist.Id, &playlist.Name, &playlist.ThumbnailPath, &playlist.Description, &playlist.CreationDate) + scanError := rows.Scan(&playlist.Id, &playlist.Name, &playlist.ThumbnailPath, &playlist.Description, &playlist.CreationDate, &playlist.IsPublic) if scanError != nil { logging.Error(fmt.Sprintf("Scan error: %s", scanError.Error())) @@ -68,9 +68,9 @@ func (table *PlaylistTable) InsertPlaylist(playlist models.Playlist) (lastInsert } func (table *PlaylistTable) DeletePlaylist(playlistId int) (error error) { - query := `DELETE FROM Playlist WHERE Id = $1` + query := `DELETE FROM Playlist WHERE Id = $1 AND IsPublic = $2` // Prevemts private playlists (like the default one) from being deleted for real - err := table.NonScalarQuery(query, playlistId) + err := table.NonScalarQuery(query, playlistId, true) if err != nil { logging.Error(fmt.Sprintf("Failed to delete playlist: %s", err.Error())) diff --git a/MyMusicBoxApi/database/songtable.go b/MyMusicBoxApi/database/songtable.go index b39be23..59a1fc7 100644 --- a/MyMusicBoxApi/database/songtable.go +++ b/MyMusicBoxApi/database/songtable.go @@ -10,6 +10,8 @@ import ( type ISongTable interface { InsertSong(song *models.Song) (err error) FetchSongs(ctx context.Context) (songs []models.Song, err error) + FetchSongById(songId int) (song models.Song, err error) + DeleteSongById(songId int) (err error) } type SongTable struct { @@ -47,6 +49,29 @@ func (table *SongTable) InsertSong(song *models.Song) (error error) { return err } +func (table *SongTable) FetchSongById(songId int) (song models.Song, err error) { + + query := "SELECT Id, Name, Path, ThumbnailPath, Duration, SourceId, UpdatedAt, CreatedAt FROM Song WHERE Id = $1" + + row := table.QueryRow(query, songId) + + _err := row.Err() + + if _err != nil { + logging.ErrorStackTrace(_err) + return models.Song{}, _err + } + + _err = row.Scan(&song.Id, &song.Name, &song.Path, &song.ThumbnailPath, &song.Duration, &song.SourceId, &song.UpdatedAt, &song.CreatedAt) + + if _err != nil { + logging.ErrorStackTrace(_err) + return models.Song{}, _err + } + + return song, nil +} + func (table *SongTable) FetchSongs(ctx context.Context) (songs []models.Song, error error) { query := "SELECT Id, Name, Path, ThumbnailPath, Duration, SourceId, UpdatedAt, CreatedAt FROM Song" // order by? @@ -77,3 +102,8 @@ func (table *SongTable) FetchSongs(ctx context.Context) (songs []models.Song, er return songs, nil } + +func (table *SongTable) DeleteSongById(songId int) (err error) { + query := "DELETE FROM Song WHERE Id = $1" + return table.NonScalarQuery(query, songId) +} diff --git a/MyMusicBoxApi/http/playlist.go b/MyMusicBoxApi/http/playlist.go index 48b9f43..69e7ce9 100644 --- a/MyMusicBoxApi/http/playlist.go +++ b/MyMusicBoxApi/http/playlist.go @@ -110,6 +110,11 @@ func (handler *PlaylistHandler) DeletePlaylist(ctx *gin.Context) { return } + if DefaultPlaylistId == id { + ctx.JSON(http.StatusInternalServerError, models.ErrorResponse("Funky music... ")) + return + } + err = handler.PlaylistTable.DeletePlaylist(id) // TODO delete background image if its not the default image for it diff --git a/MyMusicBoxApi/http/playlist_test.go b/MyMusicBoxApi/http/playlist_test.go index eef14d5..4e08d17 100644 --- a/MyMusicBoxApi/http/playlist_test.go +++ b/MyMusicBoxApi/http/playlist_test.go @@ -265,8 +265,7 @@ func TestDeletePlaylistPlaylistId(t *testing.T) { recorder := httptest.NewRecorder() - // Unable to parse to int, will throw error - _route := "/playlist/1" + _route := "/playlist/2" req, _ := http.NewRequest("DELETE", _route, nil) @@ -276,3 +275,33 @@ func TestDeletePlaylistPlaylistId(t *testing.T) { // Assert assert.Equal(t, http.StatusOK, recorder.Code) } + +func TestDeletePlaylistPlaylistIdDefaultPlaylist(t *testing.T) { + // Arrange + route := "/playlist/:playlistId" + router := SetupTestRouter() + + mockTable := &mockPlaylistTable{ + deletePlaylist: func(playlistId int) (error error) { + return nil + }, + } + + playlistHandler := PlaylistHandler{ + PlaylistTable: mockTable, + } + + router.DELETE(route, playlistHandler.DeletePlaylist) + + recorder := httptest.NewRecorder() + + _route := "/playlist/1" + + req, _ := http.NewRequest("DELETE", _route, nil) + + // Act + router.ServeHTTP(recorder, req) + + // Assert + assert.Equal(t, http.StatusInternalServerError, recorder.Code) +} diff --git a/MyMusicBoxApi/http/playlistsong.go b/MyMusicBoxApi/http/playlistsong.go index 455df23..870e135 100644 --- a/MyMusicBoxApi/http/playlistsong.go +++ b/MyMusicBoxApi/http/playlistsong.go @@ -1,19 +1,25 @@ package http import ( + "bufio" "fmt" + "musicboxapi/configuration" "musicboxapi/database" "musicboxapi/models" "net/http" + "os" "strconv" + "strings" "github.com/gin-gonic/gin" ) type PlaylistSongHandler struct { - PlaylistsongTable database.IPlaylistsongTable + PlaylistsongTable database.IPlaylistSongTable } +const DefaultPlaylistId = 1 + // @Produce json // @Param playlistId path int true "Id of playlist" // @Param lastKnowSongPosition path int false "Last song that is know by the client, pass this in to only get the latest songs" @@ -99,5 +105,74 @@ func (handler *PlaylistSongHandler) DeletePlaylistSong(ctx *gin.Context) { ctx.JSON(http.StatusInternalServerError, models.ErrorResponse(err)) return } + + // Thumbnail and .opus file will be deleted only if you delete a song via the main playlist containing all the songs + if playlistId == DefaultPlaylistId { + + songTable := database.NewSongTableInstance() + song, err := songTable.FetchSongById(songId) + + if err != nil { + ctx.JSON(http.StatusInternalServerError, models.ErrorResponse(err)) + return + } + + // Delete actual song file + audioFilePath := song.Path + configuration.DeleteFile(audioFilePath) + + // Delete actual thumbnail file + thumbnail := song.ThumbnailPath + thumbnailPath := fmt.Sprintf("%s/images/%s", configuration.Config.SourceFolder, thumbnail) + configuration.DeleteFile(thumbnailPath) + + // Delete from database + songTable.DeleteSongById(song.Id) + + // Delete from video_archive + strSplit := strings.Split(song.Path, ".") + + filenameWithOutExtension := strSplit[0] + strSplit = strings.Split(filenameWithOutExtension, "/") + + filenameWithOutExtension = strSplit[1] + + filePath := fmt.Sprintf("%s/%s", configuration.Config.SourceFolder, "video_archive") + + // Read the file + file, err := os.Open(filePath) + if err != nil { + fmt.Println("Error opening file:", err) + ctx.JSON(http.StatusInternalServerError, models.ErrorResponse(err)) + return + } + defer file.Close() + + var lines []string + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + // Keep the line only if itβs not the target + if strings.TrimSpace(line) != fmt.Sprintf("youtube %s", filenameWithOutExtension) { + lines = append(lines, line) + } + } + + if err := scanner.Err(); err != nil { + fmt.Println("Error reading file:", err) + ctx.JSON(http.StatusInternalServerError, models.ErrorResponse(err)) + return + } + + // Write the updated content back to the file + output := strings.Join(lines, "\n") + err = os.WriteFile(filePath, []byte(output+"\n"), 0644) + if err != nil { + fmt.Println("Error writing file:", err) + ctx.JSON(http.StatusInternalServerError, models.ErrorResponse(err)) + return + } + } + ctx.Status(http.StatusOK) } diff --git a/MyMusicBoxApi/http/playlistsong_test.go b/MyMusicBoxApi/http/playlistsong_test.go index ab9c3b4..679ae54 100644 --- a/MyMusicBoxApi/http/playlistsong_test.go +++ b/MyMusicBoxApi/http/playlistsong_test.go @@ -13,7 +13,7 @@ import ( ) type mockPlaylistSongTable struct { - database.IPlaylistsongTable + database.IPlaylistSongTable fetchPlaylistSongs func(ctx context.Context, playlistId int, lastKnowPosition int) (songs []models.Song, error error) insertPlaylistSong func(playlistId int, songId int) (lastInsertedId int, error error) deleteAllPlaylistSongs func(playlistId int) (error error) // TODO diff --git a/MyMusicBoxApi/models/http.go b/MyMusicBoxApi/models/http.go index e62b8c2..0f440eb 100644 --- a/MyMusicBoxApi/models/http.go +++ b/MyMusicBoxApi/models/http.go @@ -19,11 +19,13 @@ type ApiResponseModel struct { } func ErrorResponse(data any) ApiResponseModel { + return ApiResponseModel{ Data: data, Message: "An error occurred", } } + func OkResponse(data any, message string) ApiResponseModel { return ApiResponseModel{ Data: data, diff --git a/MyMusicBoxApi/reset b/MyMusicBoxApi/reset index 0d361cb..a4442e7 100755 --- a/MyMusicBoxApi/reset +++ b/MyMusicBoxApi/reset @@ -1,5 +1,8 @@ #!/bin/bash +# delete dev music and images +sudo rm -r music_dev + # stop dev docker postgres cd .. cd dev_database diff --git a/MyMusicBoxApi/service/ytdlp.go b/MyMusicBoxApi/service/ytdlp.go index 212172c..415f5dc 100644 --- a/MyMusicBoxApi/service/ytdlp.go +++ b/MyMusicBoxApi/service/ytdlp.go @@ -26,7 +26,7 @@ func StartDownloadTask(downloadRequest models.DownloadRequestModel) { config := configuration.Config storageFolderName := config.SourceFolder - archiveFileName := fmt.Sprintf("%s/video_archive", storageFolderName) + archiveFileName := fmt.Sprintf("%s/video_archive", storageFolderName) // move to env / congif idsFileName := fmt.Sprintf("%s/ids.%d", storageFolderName, parentTask.Id) namesFileName := fmt.Sprintf("%s/names.%d", storageFolderName, parentTask.Id) durationFileName := fmt.Sprintf("%s/durations.%d", storageFolderName, parentTask.Id) diff --git a/MyMusicClientSveltePwa/package-lock.json b/MyMusicClientSveltePwa/package-lock.json index 6f1d14a..3d4884b 100644 --- a/MyMusicClientSveltePwa/package-lock.json +++ b/MyMusicClientSveltePwa/package-lock.json @@ -8,6 +8,7 @@ "name": "mymusicclientsveltepwa", "version": "0.1.11", "dependencies": { + "@sveltejs/svelte-virtual-list": "^3.0.1", "@sveltestrap/sveltestrap": "^7.1.0" }, "devDependencies": { @@ -2422,6 +2423,12 @@ "acorn": "^8.9.0" } }, + "node_modules/@sveltejs/svelte-virtual-list": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/svelte-virtual-list/-/svelte-virtual-list-3.0.1.tgz", + "integrity": "sha512-aF9TptS7NKKS7/TqpsxQBSDJ9Q0XBYzBehCeIC5DzdMEgrJZpIYao9LRLnyyo6SVodpapm2B7FE/Lj+FSA5/SQ==", + "license": "LIL" + }, "node_modules/@sveltejs/vite-plugin-svelte": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.0.tgz", diff --git a/MyMusicClientSveltePwa/package.json b/MyMusicClientSveltePwa/package.json index 88d5e0a..5815fb6 100644 --- a/MyMusicClientSveltePwa/package.json +++ b/MyMusicClientSveltePwa/package.json @@ -1,7 +1,7 @@ { "name": "mymusicclientsveltepwa", "private": true, - "version": "0.1.11", + "version": "0.1.11-beta", "type": "module", "scripts": { "dev": "vite --host", @@ -18,6 +18,7 @@ "vite-plugin-pwa": "^1.0.0" }, "dependencies": { + "@sveltejs/svelte-virtual-list": "^3.0.1", "@sveltestrap/sveltestrap": "^7.1.0" } } diff --git a/MyMusicClientSveltePwa/src/App.svelte b/MyMusicClientSveltePwa/src/App.svelte index 823cc33..b497fed 100644 --- a/MyMusicClientSveltePwa/src/App.svelte +++ b/MyMusicClientSveltePwa/src/App.svelte @@ -2,8 +2,8 @@ -