diff --git a/admin/components/credits/addcredit.html b/admin/components/credits/addcredit.html index ff92595..32c43fa 100644 --- a/admin/components/credits/addcredit.html +++ b/admin/components/credits/addcredit.html @@ -12,8 +12,7 @@ hx-swap="beforeend" > - {{$Artist.Name}} - ({{$Artist.ID}}) + {{$Artist.Name}} ({{$Artist.ID}}) {{end}} diff --git a/admin/components/credits/editcredits.html b/admin/components/credits/editcredits.html index 7c51e5b..1ab9cb2 100644 --- a/admin/components/credits/editcredits.html +++ b/admin/components/credits/editcredits.html @@ -27,7 +27,7 @@ - + Delete {{end}} @@ -40,6 +40,8 @@ + diff --git a/admin/components/links/newlink.html b/admin/components/links/newlink.html deleted file mode 100644 index e69de29..0000000 diff --git a/admin/components/tracks/addtrack.html b/admin/components/tracks/addtrack.html index e69de29..f402763 100644 --- a/admin/components/tracks/addtrack.html +++ b/admin/components/tracks/addtrack.html @@ -0,0 +1,47 @@ + +
+

Add track

+
+ + + + {{if not .Tracks}} +

There are no more tracks to add.

+ {{end}} + +
+ +
+ + +
diff --git a/admin/components/tracks/edittracks.html b/admin/components/tracks/edittracks.html index e69de29..dba57a6 100644 --- a/admin/components/tracks/edittracks.html +++ b/admin/components/tracks/edittracks.html @@ -0,0 +1,112 @@ + +
+

Editing: Tracks

+ Add +
+ +
+ + +
+ + +
+
+ + +
diff --git a/admin/components/tracks/newtrack.html b/admin/components/tracks/newtrack.html index e69de29..3e76959 100644 --- a/admin/components/tracks/newtrack.html +++ b/admin/components/tracks/newtrack.html @@ -0,0 +1,9 @@ +
  • +
    +

    + {{.Number}} + {{.Title}} +

    + Delete +
    +
  • diff --git a/admin/releasehttp.go b/admin/releasehttp.go index c16bbec..8ca55b2 100644 --- a/admin/releasehttp.go +++ b/admin/releasehttp.go @@ -45,6 +45,18 @@ func serveRelease() http.Handler { case "newcredit": serveNewCredit().ServeHTTP(w, r) return + case "editlinks": + serveEditLinks(release).ServeHTTP(w, r) + return + case "edittracks": + serveEditTracks(release).ServeHTTP(w, r) + return + case "addtrack": + serveAddTrack(release).ServeHTTP(w, r) + return + case "newtrack": + serveNewTrack(release).ServeHTTP(w, r) + return } http.NotFound(w, r) return @@ -52,11 +64,11 @@ func serveRelease() http.Handler { tracks := []gatewayTrack{} for i, track := range release.Tracks { - tracks = append([]gatewayTrack{{ + tracks = append(tracks, gatewayTrack{ Track: track, Lyrics: template.HTML(strings.Replace(track.Lyrics, "\n", "
    ", -1)), - Number: len(release.Tracks) - i, - }}, tracks...) + Number: i + 1, + }) } lrw := global.LoggingResponseWriter{ResponseWriter: w, Code: http.StatusOK} @@ -121,3 +133,84 @@ func serveNewCredit() http.Handler { return }) } + +func serveEditLinks(release *model.Release) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + serveComponent(path.Join("links", "editlinks.html"), release).ServeHTTP(w, r) + return + }) +} + +func serveEditTracks(release *model.Release) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + type Track struct { + *model.Track + Number int + } + type Release struct { + *model.Release + Tracks []Track + } + var data = Release{ release, []Track{} } + for i, track := range release.Tracks { + data.Tracks = append(data.Tracks, Track{track, i + 1}) + } + + serveComponent(path.Join("tracks", "edittracks.html"), data).ServeHTTP(w, r) + return + }) +} + +func serveAddTrack(release *model.Release) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var tracks = []*model.Track{} + for _, track := range global.Tracks { + var exists = false + for _, t := range release.Tracks { + if t == track { + exists = true + break + } + } + if !exists { + tracks = append(tracks, track) + } + } + + type response struct { + ReleaseID string; + Tracks []*model.Track + } + + w.Header().Set("Content-Type", "text/html") + serveComponent(path.Join("tracks", "addtrack.html"), response{ + ReleaseID: release.ID, + Tracks: tracks, + }).ServeHTTP(w, r) + return + }) +} + +func serveNewTrack(release *model.Release) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + track := global.GetTrack(strings.Split(r.URL.Path, "/")[3]) + if track == nil { + http.NotFound(w, r) + return + } + + type Track struct { + *model.Track + Number int + } + + w.Header().Set("Content-Type", "text/html") + serveComponent(path.Join("tracks", "newtrack.html"), Track{ + track, + len(release.Tracks) + 1, + }).ServeHTTP(w, r) + return + }) +} diff --git a/admin/static/admin.css b/admin/static/admin.css index 2d6e306..9073d27 100644 --- a/admin/static/admin.css +++ b/admin/static/admin.css @@ -86,7 +86,7 @@ a img { } .card { - margin-bottom: 2em; + margin-bottom: 1em; } .card h2 { diff --git a/admin/static/admin.js b/admin/static/admin.js index e69de29..77fc383 100644 --- a/admin/static/admin.js +++ b/admin/static/admin.js @@ -0,0 +1,68 @@ +/** + * Creates a "magic" reorderable list from `container`. + * This function is absolute magic and I love it + * + * Example: + * ```html + * + * ``` + * ```js + * // javascript + * makeMagicList(document.getElementById("list"), "li"); + * ``` + * + * @param {HTMLElement} container The parent container to use as a list. + * @param {string} itemSelector The selector name of list item elements. + * @param {Function} callback A function to call after each reordering. + */ +export function makeMagicList(container, itemSelector, callback) { + if (!container) + throw new Error("container not provided"); + if (!itemSelector) + throw new Error("itemSelector not provided"); + + container.querySelectorAll(itemSelector).forEach(item => { + item.draggable = true; + item.addEventListener("dragstart", () => { item.classList.add("moving") }); + item.addEventListener("dragend", () => { item.classList.remove("moving") }); + item.querySelectorAll("input").forEach(el => { + el.addEventListener("mousedown", () => { item.draggable = false }); + el.addEventListener("mouseup", () => { item.draggable = true }); + el.addEventListener("dragstart", e => { e.stopPropagation() }); + }); + }); + + var lastCursorY; + container.addEventListener("dragover", event => { + const dragging = container.querySelector(itemSelector + ".moving"); + if (!dragging) return; + + let cursorY = event.touches ? event.touches[0].clientY : event.clientY; + + // don't bother processing if we haven't moved + if (lastCursorY === cursorY) return + lastCursorY = cursorY; + + // get the element positioned ahead of the cursor + const notMoving = [...container.querySelectorAll(itemSelector + ":not(.moving)")]; + const afterElement = notMoving.reduce((previous, current) => { + const box = current.getBoundingClientRect(); + const offset = cursorY - box.top - box.height / 2; + if (offset < 0 && offset > previous.offset) + return { offset: offset, element: current }; + return previous; + }, { offset: Number.NEGATIVE_INFINITY }).element; + + if (afterElement) { + container.insertBefore(dragging, afterElement); + } else { + container.appendChild(dragging); + } + + if (callback) callback(); + }); +} diff --git a/admin/static/release.css b/admin/static/edit-release.css similarity index 50% rename from admin/static/release.css rename to admin/static/edit-release.css index 17dabdf..30c96cb 100644 --- a/admin/static/release.css +++ b/admin/static/edit-release.css @@ -1,3 +1,9 @@ +input[type="text"] { + font-size: inherit; + font-family: inherit; + color: inherit; +} + #release { margin-bottom: 1em; padding: 1.5em; @@ -12,10 +18,6 @@ .release-artwork { width: 200px; - - display: flex; - justify-content: center; - align-items: start; } .release-artwork img { @@ -28,6 +30,7 @@ } .release-info { + width: 0; margin: 0; flex-grow: 1; display: flex; @@ -38,6 +41,28 @@ margin: 0; } +#title { + width: 100%; + margin: 0 -.2em; + padding: 0 .2em; + font-weight: bold; + border-radius: 4px; + border: 1px solid transparent; + background: transparent; + outline: none; +} + +#title:hover { + background: #ffffff; + border-color: #80808080; +} + +#title:active, +#title:focus { + background: #ffffff; + border-color: #808080; +} + .release-title small { opacity: .75; } @@ -71,6 +96,7 @@ border: none; background: none; outline: none; + resize: vertical; } .release-info table td:has(select), .release-info table td:has(input), @@ -126,6 +152,10 @@ button[disabled] { cursor: not-allowed !important; } +a.delete { + color: #d22828; +} + .release-actions { margin-top: auto; display: flex; @@ -134,90 +164,6 @@ button[disabled] { justify-content: right; } -.card.credits .credit { - margin-bottom: .5em; - padding: .5em; - display: flex; - flex-direction: row; - align-items: center; - gap: 1em; - - border-radius: .5em; - background: #f8f8f8f8; - border: 1px solid #808080; -} - -.card.credits .credit .artist-avatar { - border-radius: .5em; -} - -.card.credits .credit .artist-name { - font-weight: bold; -} - -.card.credits .credit .artist-role small { - font-size: inherit; - opacity: .66; -} - -.track { - margin-bottom: 1em; - padding: 1em; - display: flex; - flex-direction: column; - gap: .5em; - - border-radius: .5em; - background: #f8f8f8f8; - border: 1px solid #808080; -} - -.card h2.track-title { - margin: 0; - display: flex; - flex-direction: row; - justify-content: space-between; -} - -.card-title a.button { - text-decoration: none; -} - -.track-id { - width: fit-content; - font-family: "Monaspace Argon", monospace; - font-size: .8em; - font-style: italic; - line-height: 1em; - user-select: all; - -webkit-user-select: all; -} - -.track-album { - margin-left: auto; - font-style: italic; - font-size: .75em; - opacity: .5; -} - -.track-album.empty { - color: #ff2020; - opacity: 1; -} - -.track-description { - font-style: italic; -} - -.track-lyrics { - max-height: 10em; - overflow-y: scroll; -} - -.track .empty { - opacity: 0.75; -} - dialog { width: min(720px, calc(100% - 2em)); padding: 2em; @@ -245,13 +191,15 @@ dialog div.dialog-actions { gap: .5em; } -dialog#editcredits ul { - margin: 0; - padding: 0; - list-style: none; +.card-title a.button { + text-decoration: none; } -dialog#editcredits .credit>div { +/* + * RELEASE CREDITS + */ + +.card.credits .credit { margin-bottom: .5em; padding: .5em; display: flex; @@ -264,24 +212,70 @@ dialog#editcredits .credit>div { border: 1px solid #808080; } -dialog#editcredits .credit p { - margin: 0; -} - -dialog#editcredits .credit .artist-avatar { +.card.credits .credit .artist-avatar { border-radius: .5em; } -dialog#editcredits .credit .credit-info { +.card.credits .credit .artist-name { + font-weight: bold; +} + +.card.credits .credit .artist-role small { + font-size: inherit; + opacity: .66; +} + +#editcredits ul { + margin: 0; + padding: 0; + list-style: none; +} + +#editcredits .credit>div { + margin-bottom: .5em; + padding: .5em; + display: flex; + flex-direction: row; + align-items: center; + gap: 1em; + + border-radius: .5em; + background: #f8f8f8f8; + border: 1px solid #808080; +} + +#editcredits .credit { + transition: transform .2s ease-out, opacity .2s; +} + +#editcredits .credit.moving { + transform: scale(1.05); + opacity: .5; +} + +#editcredits .credit p { + margin: 0; +} + +#editcredits .credit .artist-avatar { + border-radius: .5em; +} + +#editcredits .credit .credit-info { width: 100%; } -dialog#editcredits .credit .credit-info .credit-attribute { +#editcredits .credit .credit-info .credit-attribute { width: 100%; display: flex; } -dialog#editcredits .credit .credit-info .credit-attribute input[type="text"] { +#editcredits .credit .credit-info .credit-attribute label { + display: flex; + align-items: center; +} + +#editcredits .credit .credit-info .credit-attribute input[type="text"] { margin-left: .25em; padding: .2em .4em; flex-grow: 1; @@ -291,15 +285,255 @@ dialog#editcredits .credit .credit-info .credit-attribute input[type="text"] { color: inherit; } -dialog#editcredits .credit .artist-name { +#editcredits .credit .artist-name { font-weight: bold; } -dialog#editcredits .credit .artist-role small { +#editcredits .credit .artist-role small { font-size: inherit; opacity: .66; } -dialog#editcredits .credit button.delete { +#editcredits .credit button.delete { margin-left: auto; } + +#addcredit ul { + padding: 0; + list-style: none; + background: #f8f8f8; +} + +#addcredit ul li.new-artist { + padding: .5em; + display: flex; + gap: .5em; + cursor: pointer; +} + +#addcredit ul li.new-artist:nth-child(even) { + background: #f0f0f0; +} + +#addcredit ul li.new-artist:hover { + background: #e0e0e0; +} + +#addcredit .new-artist .artist-id { + opacity: .5; +} + +/* + * RELEASE LINKS + */ + +.card.links { + display: flex; + gap: .2em; +} + +.card.links a.button[data-name="spotify"] { + background-color: #8cff83 +} + +.card.links a.button[data-name="applemusic"] { + background-color: #8cd9ff +} + +.card.links a.button[data-name="soundcloud"] { + background-color: #fdaa6d +} + +.card.links a.button[data-name="youtube"] { + background-color: #ff6e6e +} + +#editlinks table { + width: 100%; +} + +#editlinks tr { + display: flex; +} + +#editlinks th { + padding: 0 .1em; + display: flex; + align-items: center; + text-align: left; +} + +#editlinks tr:nth-child(odd) { + background: #f8f8f8; +} + +#editlinks tr th, +#editlinks tr td { + height: 2em; +} + +#editlinks tr td { + padding: 0; +} + +#editlinks tr.link { + transition: transform .2s ease-out, opacity .2s; +} + +#editlinks tr.link.moving { + transform: scale(1.05); + opacity: .5; +} + +#editlinks tr .grabber { + width: 2em; + display: flex; + justify-content: center; + cursor: pointer; +} +#editlinks tr .grabber img { + width: 1em; + pointer-events: none; +} +#editlinks tr .link-name { + width: 8em; +} +#editlinks tr .link-url { + flex-grow: 1; +} + +#editlinks td a.delete { + display: flex; + height: 100%; + align-items: center; + padding: 0 .5em; +} + +#editlinks td input[type="text"] { + width: calc(100% - .6em); + height: 100%; + padding: 0 .3em; + border: none; + outline: none; + cursor: pointer; + background: none; +} +#editlinks td input[type="text"]:hover { + background: #0001; +} +#editlinks td input[type="text"]:focus { + outline: 1px solid #808080; +} + +/* + * RELEASE TRACKS + */ + +.card.tracks .track { + margin-bottom: 1em; + padding: 1em; + display: flex; + flex-direction: column; + gap: .5em; + + border-radius: .5em; + background: #f8f8f8f8; + border: 1px solid #808080; +} + +.card.tracks h2.track-title { + margin: 0; + display: flex; + gap: .5em; +} + +.card.tracks h2.track-title .track-number { + opacity: .5; +} + +.card.tracks .track-album { + margin-left: auto; + font-style: italic; + font-size: .75em; + opacity: .5; +} + +.card.tracks .track-album.empty { + color: #ff2020; + opacity: 1; +} + +.card.tracks .track-description { + font-style: italic; +} + +.card.tracks .track-lyrics { + max-height: 10em; + overflow-y: scroll; +} + +.card.tracks .track .empty { + opacity: 0.75; +} + +#edittracks ul { + padding: 0; + list-style: none; +} + +#edittracks .track { + transition: transform .2s ease-out, opacity .2s; +} + +#edittracks .track.moving { + transform: scale(1.05); + opacity: .5; +} + +#edittracks .track div { + padding: .5em; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + cursor: pointer; +} + +#edittracks .track div:active { + cursor: move; +} + +#edittracks .track:nth-child(even) { + background: #f0f0f0; +} + +#edittracks .track-number { + min-width: 1em; + display: inline-block; + opacity: .5; +} + +#edittracks .track-name { + margin: 0; +} + +#addtrack ul { + padding: 0; + list-style: none; + background: #f8f8f8; +} + +#addtrack ul li.new-track { + padding: .5em; + display: flex; + gap: .5em; + cursor: pointer; +} + +#addtrack ul li.new-track:nth-child(even) { + background: #f0f0f0; +} + +#addtrack ul li.new-track:hover { + background: #e0e0e0; +} diff --git a/admin/static/edit-release.js b/admin/static/edit-release.js index 7be9069..1e9fa29 100644 --- a/admin/static/edit-release.js +++ b/admin/static/edit-release.js @@ -1,7 +1,9 @@ import Stateful from "/script/silver.min.js" const releaseID = document.getElementById("release").dataset.id; -const artwork_input = document.getElementById("artwork"); +const title_input = document.getElementById("title"); +const artwork_img = document.getElementById("artwork"); +const artwork_input = document.getElementById("artwork-file"); const type_input = document.getElementById("type"); const desc_input = document.getElementById("description"); const date_input = document.getElementById("release-date"); @@ -10,20 +12,22 @@ const buylink_input = document.getElementById("buylink"); const vis_input = document.getElementById("visibility"); const save_btn = document.getElementById("save"); -let token = atob(localStorage.getItem("arime-token")); +var artwork_data = artwork_img.attributes.src.value; -let edited = new Stateful(false); +var token = atob(localStorage.getItem("arime-token")); -let release_data = update_data(undefined); +var edited = new Stateful(false); + +var release_data = update_data(undefined); function update_data(old) { - let release_data = { + var release_data = { visible: vis_input.value === "true", - title: undefined, + title: title_input.value, description: desc_input.value, type: type_input.value, releaseDate: date_input.value, - artwork: artwork_input.attributes.src.value, + artwork: artwork_data, buyname: buyname_input.value, buylink: buylink_input.value, }; @@ -38,8 +42,6 @@ function update_data(old) { function save_release() { console.table(release_data); - edited.set(false); - (async () => { const res = await fetch( "/api/v1/music/" + releaseID, { @@ -61,15 +63,29 @@ function save_release() { location = location; })(); } -window.save_release = save_release; edited.onUpdate(edited => { save_btn.disabled = !edited; }) -artwork_input.addEventListener("click", () => { +title_input.addEventListener("change", () => { release_data = update_data(release_data); }); +artwork_img.addEventListener("click", () => { + artwork_input.addEventListener("change", () => { + if (artwork_input.files.length > 0) { + const reader = new FileReader(); + reader.onload = e => { + const data = e.target.result; + artwork_img.src = data; + artwork_data = data; + release_data = update_data(release_data); + }; + reader.readAsDataURL(artwork_input.files[0]); + } + }); + artwork_input.click(); +}); type_input.addEventListener("change", () => { release_data = update_data(release_data); }); diff --git a/admin/static/index.js b/admin/static/index.js new file mode 100644 index 0000000..709f902 --- /dev/null +++ b/admin/static/index.js @@ -0,0 +1,24 @@ +const newReleaseBtn = document.getElementById("create-release"); + +newReleaseBtn.addEventListener("click", event => { + event.preventDefault(); + const id = prompt("Enter an ID for this release:"); + if (id == null || id == "") return; + + fetch("/api/v1/music", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({id}) + }).then(res => { + if (res.ok) location = "/admin/release/" + id; + else { + res.text().then(err => { + alert("Request failed: " + err); + console.error(err); + }); + } + }).catch(err => { + alert("Failed to create release. Check the console for details."); + console.error(err); + }); +}); diff --git a/admin/views/edit-release.html b/admin/views/edit-release.html index 375bbd2..d33c8e6 100644 --- a/admin/views/edit-release.html +++ b/admin/views/edit-release.html @@ -2,7 +2,7 @@ editing {{.Title}} - ari melody 💫 - + {{end}} {{define "content"}} @@ -11,18 +11,13 @@
    +

    - - {{.Title}} - {{.GetReleaseYear}} +

    - - - - @@ -130,9 +125,7 @@ @@ -148,13 +141,19 @@
    {{range .Tracks}}
    -

    {{.Number}}. {{.Title}}

    -

    {{.ID}}

    +

    + {{.Number}} + {{.Title}} +

    + +

    Description

    {{if .Description}}

    {{.Description}}

    {{else}}

    No description provided.

    {{end}} + +

    Lyrics

    {{if .Lyrics}}

    {{.Lyrics}}

    {{else}} diff --git a/admin/views/index.html b/admin/views/index.html index 8847492..36dff67 100644 --- a/admin/views/index.html +++ b/admin/views/index.html @@ -9,7 +9,7 @@

    Releases

    - Create New + Create New
    {{range $Release := .Releases}} @@ -93,5 +93,6 @@ - + + {{end}} diff --git a/api/release.go b/api/release.go index 6d02733..2064fb0 100644 --- a/api/release.go +++ b/api/release.go @@ -1,9 +1,14 @@ package api import ( + "bufio" + "encoding/base64" "encoding/json" "fmt" + "io/fs" "net/http" + "os" + "path/filepath" "strings" "time" @@ -85,40 +90,53 @@ func CreateRelease() http.Handler { http.Error(w, "Release ID cannot be empty\n", http.StatusBadRequest) return } - if *data.Title == "" { - http.Error(w, "Release title cannot be empty\n", http.StatusBadRequest) - return + + title := data.ID + if data.Title != nil && *data.Title != "" { + title = *data.Title } - if data.Buyname == nil || *data.Buyname == "" { *data.Buyname = "buy" } - if data.Buylink == nil || *data.Buylink == "" { *data.Buylink = "https://arimelody.me" } + + description := "" + if data.Description != nil && *data.Description != "" { description = *data.Description } + + releaseType := model.Single + if data.ReleaseType != nil && *data.ReleaseType != "" { releaseType = *data.ReleaseType } + + releaseDate := time.Time{} + if data.ReleaseDate != nil && *data.ReleaseDate != "" { + releaseDate, err = time.Parse("2006-01-02T15:04", *data.ReleaseDate) + if err != nil { + http.Error(w, "Invalid release date", http.StatusBadRequest) + return + } + } else { + releaseDate = time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC) + } + + artwork := "/img/default-cover-art.png" + if data.Artwork != nil && *data.Artwork != "" { artwork = *data.Artwork } + + buyname := "" + if data.Buyname != nil && *data.Buyname != "" { buyname = *data.Buyname } + + buylink := "" + if data.Buylink != nil && *data.Buylink != "" { buylink = *data.Buylink } if global.GetRelease(data.ID) != nil { http.Error(w, fmt.Sprintf("Release %s already exists\n", data.ID), http.StatusBadRequest) return } - releaseDate := time.Time{} - if *data.ReleaseDate == "" { - http.Error(w, "Release date cannot be empty\n", http.StatusBadRequest) - return - } else if data.ReleaseDate != nil { - releaseDate, err = time.Parse("2006-01-02T15:04", *data.ReleaseDate) - if err != nil { - http.Error(w, "Invalid release date", http.StatusBadRequest) - return - } - } - var release = model.Release{ ID: data.ID, - Visible: *data.Visible, - Title: *data.Title, - Description: *data.Description, - ReleaseType: *data.ReleaseType, + Visible: false, + Title: title, + Description: description, + ReleaseType: releaseType, ReleaseDate: releaseDate, - Artwork: *data.Artwork, - Buyname: *data.Buyname, - Buylink: *data.Buylink, + Artwork: artwork, + Buyname: buyname, + Buylink: buylink, Links: []*model.Link{}, Credits: []*model.Credit{}, Tracks: []*model.Track{}, @@ -181,7 +199,69 @@ func UpdateRelease() http.Handler { } update.ReleaseDate = newDate } - if data.Artwork != nil { update.Artwork = *data.Artwork } + if data.Artwork != nil { + if strings.Contains(*data.Artwork, ";base64,") { + split := strings.Split(*data.Artwork, ";base64,") + header := split[0] + imageData, err := base64.StdEncoding.DecodeString(split[1]) + ext, _ := strings.CutPrefix(header, "data:image/") + + switch ext { + case "png": + case "jpg": + case "jpeg": + default: + http.Error(w, "Invalid image type. Allowed: .png, .jpg, .jpeg", http.StatusBadRequest) + return + } + + artworkDirectory := filepath.Join("uploads", "musicart") + // ensure directory exists + os.MkdirAll(artworkDirectory, os.ModePerm) + + imagePath := filepath.Join(artworkDirectory, fmt.Sprintf("%s.%s", update.ID, ext)) + file, err := os.Create(imagePath) + if err != nil { + fmt.Printf("FATAL: Failed to create file %s: %s\n", imagePath, err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + defer file.Close() + + buffer := bufio.NewWriter(file) + _, err = buffer.Write(imageData) + if err != nil { + fmt.Printf("FATAL: Failed to write to file %s: %s\n", imagePath, err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + if err := buffer.Flush(); err != nil { + fmt.Printf("FATAL: Failed to flush data to file %s: %s\n", imagePath, err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + // clean up files with this ID and different extensions + err = filepath.Walk(artworkDirectory, func(path string, info fs.FileInfo, err error) error { + if path == imagePath { return nil } + + withoutExt := strings.TrimSuffix(path, filepath.Ext(path)) + if withoutExt != filepath.Join(artworkDirectory, update.ID) { return nil } + + return os.Remove(path) + }) + if err != nil { + fmt.Printf("WARN: Error while cleaning up artwork files: %s\n", err) + } + + fmt.Printf("Artwork for %s updated.\n", update.ID) + update.Artwork = fmt.Sprintf("/uploads/musicart/%s.%s", update.ID, ext) + } else { + update.Artwork = *data.Artwork + } + } if data.Buyname != nil { if *data.Buyname == "" { http.Error(w, "Release buy name cannot be empty", http.StatusBadRequest) diff --git a/music/controller/release.go b/music/controller/release.go index 9e70aa7..167f4b6 100644 --- a/music/controller/release.go +++ b/music/controller/release.go @@ -47,7 +47,7 @@ func PullReleaseTracksDB(db *sqlx.DB, release *model.Release) ([]*model.Track, e err := db.Select(&track_rows, "SELECT track FROM musicreleasetrack "+ "WHERE release=$1 "+ - "ORDER BY number DESC", + "ORDER BY number ASC", release.ID, ) if err != nil { diff --git a/music/view/release.go b/music/view/release.go index 634c481..6b9b181 100644 --- a/music/view/release.go +++ b/music/view/release.go @@ -86,11 +86,11 @@ func ServeGateway() http.Handler { tracks := []gatewayTrack{} for i, track := range release.Tracks { - tracks = append([]gatewayTrack{{ + tracks = append(tracks, gatewayTrack{ Track: track, Lyrics: template.HTML(strings.Replace(track.Lyrics, "\n", "
    ", -1)), - Number: len(release.Tracks) - i, - }}, tracks...) + Number: i + 1, + }) } lrw := global.LoggingResponseWriter{ResponseWriter: w, Code: http.StatusOK} diff --git a/public/img/list-grabber.svg b/public/img/list-grabber.svg new file mode 100644 index 0000000..58eb280 --- /dev/null +++ b/public/img/list-grabber.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/res/list-grabber.afdesign b/res/list-grabber.afdesign new file mode 100644 index 0000000..9184153 Binary files /dev/null and b/res/list-grabber.afdesign differ diff --git a/res/list-grabber.afdesign~lock~ b/res/list-grabber.afdesign~lock~ new file mode 100644 index 0000000..663b6ef Binary files /dev/null and b/res/list-grabber.afdesign~lock~ differ diff --git a/views/music-gateway.html b/views/music-gateway.html index 57e2434..0eb4081 100644 --- a/views/music-gateway.html +++ b/views/music-gateway.html @@ -64,6 +64,7 @@

    Releases: {{.PrintReleaseDate}}

    {{end}} + {{if .IsReleased}} + {{end}} {{if .Description}}

    diff --git a/views/music.html b/views/music.html index 225a9a9..8e59903 100644 --- a/views/music.html +++ b/views/music.html @@ -38,6 +38,7 @@

    {{$Release.PrintArtists true true}}

    {{$Release.ReleaseType}}

    + {{if $Release.IsReleased}} + {{end}}
    {{end}}
    Artists{{.PrintArtists true true}}
    Type @@ -50,7 +45,7 @@ name="Description" value="{{.Description}}" placeholder="No description provided." - rows="3" + rows="1" id="description" >{{.Description}}