Custom paintings and discs need two packs, and installing one of them is the usual failure
The datapack registers the content; the resource pack supplies the texture and the sound. Datapack alone and it registers with nothing to show. Plus the 4×4 limit and why disc length is not cosmetic.
Paintings and music discs are both real registries in modern Java, which means you can add them rather than replace vanilla ones. Nothing you add displaces an existing painting or disc.
The catch is that the content is split across two different pack types, and each is useless alone.
What goes where
The datapack registers the thing:
data/<ns>/painting_variant/<id>.json asset_id, width, height, title, author
data/<ns>/jukebox_song/<id>.json sound_event, description, length, comparator
The resource pack supplies what you see and hear:
assets/<ns>/textures/painting/<id>.png
assets/<ns>/sounds/<id>.ogg
assets/<ns>/sounds.json
Install only the datapack and the content registers successfully — it exists, /give finds it, and there is simply no texture and no audio. That is a much more confusing outcome than an outright failure, and it is the usual reason a custom painting "didn't work". The tool on this page ships both in one zip specifically so they cannot be separated.
Note the two pack.mcmeta files. They are separate packs with separate identities; there is no combined format that holds both.
Registry ids are stricter than filenames
An id may contain only a-z, 0-9, _, . and -. Anything else and the pack fails to load — not the one broken entry, the pack. Capital letters and spaces are the two that catch people, because both are perfectly ordinary in the filename you dragged in.
Paintings: 1×1 to 4×4
Vanilla sizes run from 1 × 1 up to 4 × 4 blocks, giving sixteen possible shapes. A variant outside that range will still render — the game does not reject it — but it cannot be hung on a wall of the size players expect, so it is effectively unplaceable in a normal build.
The size is declared in the JSON as width and height, in blocks. It is not inferred from the PNG, so a 3 × 1 painting whose texture is square is a valid file that displays stretched.
Disc length is not a label
length_in_seconds drives two things: the jukebox timing, and the comparator ramp — the signal a comparator reads from a playing jukebox rises across the track's declared duration.
So an inaccurate length is not a cosmetic error. Declare a 200-second track as 100 and the comparator finishes its ramp halfway through, which quietly breaks any redstone keyed to a disc ending. This tool reads the real duration out of the file rather than asking you to type it, for exactly that reason.
Tracks are also written with "stream": true in sounds.json. Music is long, and loading a full track into memory before playing it stutters — streaming is the right default for anything disc-length, and the wrong one for a short sound effect.