mirror of
https://github.com/riwiwa/muzi.git
synced 2026-03-04 00:51:59 -08:00
add scrobble removal
This commit is contained in:
@@ -794,6 +794,7 @@ func GetHistoryForSong(userId, songId int, limit, offset int) ([]ScrobbleEntry,
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ScrobbleEntry struct {
|
type ScrobbleEntry struct {
|
||||||
|
Id int
|
||||||
Timestamp time.Time
|
Timestamp time.Time
|
||||||
SongName string
|
SongName string
|
||||||
ArtistName string
|
ArtistName string
|
||||||
@@ -958,7 +959,7 @@ func GetHistoryForSongs(userId int, songIds []int, limit, offset int) ([]Scrobbl
|
|||||||
return []ScrobbleEntry{}, nil
|
return []ScrobbleEntry{}, nil
|
||||||
}
|
}
|
||||||
rows, err := Pool.Query(context.Background(),
|
rows, err := Pool.Query(context.Background(),
|
||||||
`SELECT h.timestamp, h.song_name, h.album_name, h.ms_played, h.platform,
|
`SELECT h.id, h.timestamp, h.song_name, h.album_name, h.ms_played, h.platform,
|
||||||
(SELECT name FROM artists WHERE id = h.artist_id) as artist_name,
|
(SELECT name FROM artists WHERE id = h.artist_id) as artist_name,
|
||||||
h.artist_ids
|
h.artist_ids
|
||||||
FROM history h WHERE h.user_id = $1 AND h.song_id = ANY($2)
|
FROM history h WHERE h.user_id = $1 AND h.song_id = ANY($2)
|
||||||
@@ -972,7 +973,7 @@ func GetHistoryForSongs(userId int, songIds []int, limit, offset int) ([]Scrobbl
|
|||||||
var entries []ScrobbleEntry
|
var entries []ScrobbleEntry
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var e ScrobbleEntry
|
var e ScrobbleEntry
|
||||||
err := rows.Scan(&e.Timestamp, &e.SongName, &e.AlbumName, &e.MsPlayed, &e.Platform, &e.ArtistName, &e.ArtistIds)
|
err := rows.Scan(&e.Id, &e.Timestamp, &e.SongName, &e.AlbumName, &e.MsPlayed, &e.Platform, &e.ArtistName, &e.ArtistIds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -980,3 +981,17 @@ func GetHistoryForSongs(userId int, songIds []int, limit, offset int) ([]Scrobbl
|
|||||||
}
|
}
|
||||||
return entries, nil
|
return entries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DeleteHistoryByIds(userId int, ids []int) error {
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_, err := Pool.Exec(context.Background(),
|
||||||
|
`DELETE FROM history WHERE user_id = $1 AND id = ANY($2)`,
|
||||||
|
userId, ids)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error deleting history: %v\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
{{.Song.Title}}
|
{{.Song.Title}}
|
||||||
{{if eq .LoggedInUsername .Username}}
|
{{if eq .LoggedInUsername .Username}}
|
||||||
<button class="edit-btn" onclick="openEditModal()">Edit</button>
|
<button class="edit-btn" onclick="openEditModal()">Edit</button>
|
||||||
|
<button class="edit-btn" id="removeScrobblesBtn" onclick="toggleRemoveMode()">Remove</button>
|
||||||
{{end}}
|
{{end}}
|
||||||
</h1>
|
</h1>
|
||||||
{{if .ArtistNames}}
|
{{if .ArtistNames}}
|
||||||
@@ -23,9 +24,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="history">
|
<div class="history">
|
||||||
<h3>Scrobbles</h3>
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||||
|
<h3>Scrobbles</h3>
|
||||||
|
<div id="removeControls" style="display: none;">
|
||||||
|
<button class="edit-btn" onclick="cancelRemoveMode()">Cancel</button>
|
||||||
|
<button class="edit-btn" onclick="deleteSelectedScrobbles()" style="background: #c44;">Delete Selected</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th class="remove-checkbox-col" style="display: none;"><input type="checkbox" id="selectAllCheckboxes"></th>
|
||||||
<th>Artist</th>
|
<th>Artist</th>
|
||||||
<th>Title</th>
|
<th>Title</th>
|
||||||
<th>Album</th>
|
<th>Album</th>
|
||||||
@@ -34,6 +42,9 @@
|
|||||||
{{$username := .Username}}
|
{{$username := .Username}}
|
||||||
{{range .Times}}
|
{{range .Times}}
|
||||||
<tr>
|
<tr>
|
||||||
|
<td class="remove-checkbox-col" style="display: none;">
|
||||||
|
<input type="checkbox" class="scrobble-checkbox" value="{{.Id}}">
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{- $artistNames := getArtistNames .ArtistIds}}
|
{{- $artistNames := getArtistNames .ArtistIds}}
|
||||||
{{- range $i, $name := $artistNames}}{{if $i}}, {{end}}<a href="/profile/{{$username}}/artist/{{urlquery $name}}">{{$name}}</a>{{end}}
|
{{- range $i, $name := $artistNames}}{{if $i}}, {{end}}<a href="/profile/{{$username}}/artist/{{urlquery $name}}">{{$name}}</a>{{end}}
|
||||||
@@ -68,4 +79,67 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleRemoveMode() {
|
||||||
|
var checkboxes = document.querySelectorAll('.remove-checkbox-col');
|
||||||
|
checkboxes.forEach(function(col) {
|
||||||
|
col.style.display = '';
|
||||||
|
});
|
||||||
|
document.getElementById('removeScrobblesBtn').style.display = 'none';
|
||||||
|
document.getElementById('removeControls').style.display = '';
|
||||||
|
|
||||||
|
var selectAll = document.getElementById('selectAllCheckboxes');
|
||||||
|
selectAll.addEventListener('change', function() {
|
||||||
|
document.querySelectorAll('.scrobble-checkbox').forEach(function(cb) {
|
||||||
|
cb.checked = selectAll.checked;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelRemoveMode() {
|
||||||
|
var checkboxes = document.querySelectorAll('.remove-checkbox-col');
|
||||||
|
checkboxes.forEach(function(col) {
|
||||||
|
col.style.display = 'none';
|
||||||
|
});
|
||||||
|
document.getElementById('removeScrobblesBtn').style.display = '';
|
||||||
|
document.getElementById('removeControls').style.display = 'none';
|
||||||
|
|
||||||
|
document.querySelectorAll('.scrobble-checkbox').forEach(function(cb) {
|
||||||
|
cb.checked = false;
|
||||||
|
});
|
||||||
|
document.getElementById('selectAllCheckboxes').checked = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteSelectedScrobbles() {
|
||||||
|
var checkboxes = document.querySelectorAll('.scrobble-checkbox:checked');
|
||||||
|
var ids = [];
|
||||||
|
checkboxes.forEach(function(cb) {
|
||||||
|
ids.push(parseInt(cb.value));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ids.length === 0) {
|
||||||
|
alert('Please select at least one scrobble to delete.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm('Are you sure you want to delete ' + ids.length + ' scrobble(s)?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', '/api/scrobble/delete', true);
|
||||||
|
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||||
|
xhr.onreadystatechange = function() {
|
||||||
|
if (xhr.readyState === 4) {
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Error deleting scrobbles: ' + xhr.responseText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.send(JSON.stringify(ids));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -1002,3 +1002,36 @@ func imageUploadHandler() http.HandlerFunc {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func deleteScrobbleHandler() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
username := getLoggedInUsername(r)
|
||||||
|
if username == "" {
|
||||||
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userId, err := getUserIdByUsername(r.Context(), username)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "User not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var ids []int
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&ids); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error decoding request: %v\n", err)
|
||||||
|
http.Error(w, "Invalid request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.DeleteHistoryByIds(userId, ids)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error deleting scrobbles: %v\n", err)
|
||||||
|
http.Error(w, "Error deleting scrobbles", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ func Start() {
|
|||||||
r.Patch("/api/artist/{id}/batch", artistBatchEditHandler())
|
r.Patch("/api/artist/{id}/batch", artistBatchEditHandler())
|
||||||
r.Patch("/api/song/{id}/batch", songBatchEditHandler())
|
r.Patch("/api/song/{id}/batch", songBatchEditHandler())
|
||||||
r.Patch("/api/album/{id}/batch", albumBatchEditHandler())
|
r.Patch("/api/album/{id}/batch", albumBatchEditHandler())
|
||||||
|
r.Post("/api/scrobble/delete", deleteScrobbleHandler())
|
||||||
r.Post("/api/upload/image", imageUploadHandler())
|
r.Post("/api/upload/image", imageUploadHandler())
|
||||||
r.Get("/search", searchHandler())
|
r.Get("/search", searchHandler())
|
||||||
r.Get("/import", importPageHandler())
|
r.Get("/import", importPageHandler())
|
||||||
|
|||||||
Reference in New Issue
Block a user