feat(Melon): Implement Melon provider - #234
Conversation
Melon is a South Korean digital streaming service. This provider uses the mobile app's API, which is publicly accessible without authentication. The API returns album metadata and track listings via separate endpoints. The brand color and icon have been taken from the website. To implement this change, I used the help of an AI tool (namely Claude Code using the Sonnet 4.6 model), which created the Melon API types from HTTP responses caputured with curl using the endpoints described in kellnerd#203, and wrote the initial provider implementation (using the previous Bugs! provider as a reference). Regardless, all code changes have been personally reviewed by me, and I take full responsibility for them. I also selected and wrote the test cases myself. Assisted-by: Claude:claude-sonnet-4.6
|
Still plan to do some fine-tuning on the provider implementation and fix the ToDo I left there, but I think it is mostly ready to review already. |
kellnerd
left a comment
There was a problem hiding this comment.
Thank you for giving this a shot. Sorry that it took quite a while till I came back to reviewing it, I needed a short break after your last PR and then it got too hot and my motivation was melting like ice in the sun.
Without having tried the provider so far, here is some initial feedback.
| ); | ||
| return { | ||
| url: originalUrl, | ||
| thumbUrl: largeUrl, |
There was a problem hiding this comment.
I would consider using the 500px image for the thumbnail, most of the other provider thumbnails are even only half of that..
|
|
||
| readonly supportedUrls = new URLPattern({ | ||
| hostname: 'www.melon.com', | ||
| pathname: String.raw`/:type(album|song|artist)/detail.htm\?\1Id=:id(\d+)`, |
There was a problem hiding this comment.
Interesting, I wouldn't have thought that this works, the search query is a separate property of the URLPattern usually. I have an unpublished provider which extracts the ID from the query that way, but this surely wouldn't allow us to have a regex back reference.
| MelonSongListResponse, | ||
| } from './api_types.ts'; | ||
|
|
||
| const API_BASE = 'https://m2.melon.com/m6'; |
There was a problem hiding this comment.
| const API_BASE = 'https://m2.melon.com/m6'; | |
| const API_BASE = 'https://m2.melon.com/m6/'; |
allows us to use the two argument URL constructor.
|
|
||
| export class MelonReleaseLookup extends ReleaseApiLookup<MelonProvider, MelonRawRelease> { | ||
| constructReleaseApiUrl(): URL { | ||
| const url = new URL(`${API_BASE}/v3/album/info.json`); |
There was a problem hiding this comment.
| const url = new URL(`${API_BASE}/v3/album/info.json`); | |
| const url = new URL('v3/album/info.json', API_BASE); |
| const albumId = this.lookup.value; | ||
|
|
||
| const infoUrl = this.constructReleaseApiUrl(); | ||
| const songListUrl = new URL(`${API_BASE}/v2/album/song/list.json`); |
There was a problem hiding this comment.
| const songListUrl = new URL(`${API_BASE}/v2/album/song/list.json`); | |
| const songListUrl = new URL('v2/album/song/list.json', API_BASE); |
| ISSERVICE: boolean; | ||
| /** Whether the song is marked as a title song, which is usually actively promoted, has an MV, and/or was previously released as a single. */ | ||
| ISTITLESONG: boolean; | ||
| ISHOLDBACK: boolean; |
There was a problem hiding this comment.
Have you seen examples where ISSERVICE and ISHOLDBACK indicate unavailable tracks/releases or something else which is of interest?
There is also an ISFREE attribute in the testdata which you haven't used here.
|
|
||
| private extractLabels(planCnpy?: string): Label[] { | ||
| if (!planCnpy) return []; | ||
| return planCnpy.split(', ').map((name) => ({ name: name.trim() })); |
There was a problem hiding this comment.
We already have a utility function splitLabels which should be reused. Feel free to enhance its test cases with a Korean script example.
| cdList: MelonDisc[]; | ||
| } | ||
|
|
||
| function parseISSUEDATE(date: string): PartialDate { |
There was a problem hiding this comment.
We should revisit this helper as part of #169 one day 😇
| if (albumType === 'EP') return ['EP']; | ||
| if (albumType === '옴니버스') return ['Compilation']; | ||
| if (albumType === 'OST') return ['Soundtrack']; | ||
| if (albumType === '리믹스') return ['Single', 'Remix']; |
There was a problem hiding this comment.
Could you confirm the theory that this is only used for remix singles and not remix albums?
| > | ||
| <path | ||
| fillRule='evenodd' | ||
| d='M3.89 15.25A8.0 7.95 0 1 0 19.89 15.25A8.0 7.95 0 1 0 3.89 15.25ZM8.34 15.25A3.55 3.84 0 1 0 15.44 15.25A3.55 3.84 0 1 0 8.34 15.25ZM15.3 3.7A2.95 2.95 0 1 0 21.2 3.7A2.95 2.95 0 1 0 15.3 3.7Z' |
There was a problem hiding this comment.
Nice and simple logo.
I do roughly understand SVG path syntax and wonder whether it is intentional that the logo does not consist of perfect circles (A8.0 7.95 is x and y radius for example) or a conversion/rounding error at some point.
Not worth changing that right now, one day I may make Tabler-style icons for some of the providers and submit them upstream like I did for the MusicBrainz icon.
Melon is a South Korean digital streaming service. This provider uses the mobile app's API, which is publicly accessible without authentication. The API returns album metadata and track listings via separate endpoints. The brand color and icon have been taken from the website.
To implement this change, I used the help of an AI tool (namely Claude Code using the Sonnet 4.6 model), which created the Melon API types from HTTP responses caputured with curl using the endpoints described in #203, and wrote the initial provider implementation (using the previous Bugs! provider as a reference). Regardless, all code changes have been personally reviewed by me, and I take full responsibility for them. I also selected and wrote the test cases myself.
Assisted-by: Claude:claude-sonnet-4.6
Closes #203.