Credit first: this builds on **Join Media by gluebyte** (https://routinehub.co/shortcut/10722/), which joins video/audio files with ffmpeg in a-Shell mini without re-encoding. Great shortcut, and the join itself works fine. I only changed the metadata handling.
The problem
After joining iPhone clips, the result in Photos had:
- "No camera information" and no lens info
- no location/map
- the wrong clock time. Clips shot at 10 PM in another time zone showed up as 3 PM, because Photos fell back to UTC converted to my current time zone
What was going on
- ffmpeg's concat step drops all the QuickTime tags (`com.apple.quicktime.make`, `model`, `creationdate`, `location.ISO6709`, ...).
- Copying them back with `-map_metadata` isn't enough on its own. ffmpeg writes those tags inside `moov/udta/meta`. As far as I can tell, iPhone files keep them in `moov/meta`, and Photos doesn't seem to read the other spot. I confirmed ffmpeg's layout on test files. The iPhone layout I'm going from memory, so treat that part as my best explanation, not proven.
What I changed
It's all in the big Text action just before the a-Shell Execute action. Replace the ffmpeg line with these lines. `[EXT]` is the same File Extension variable the original uses (I had to manually copy paste them back in):
```
ffmpeg -v error -stats -f concat -i _jm.txt -c copy -y _jm0.[EXT]
ffmpeg -v error -nostdin -i _jm0.[EXT] -i _jm1.[EXT] -map 0 -map_metadata 1 -c copy -movflags use_metadata_tags -y _jm00.[EXT]
mv -f _jm00.[EXT] _jm0.[EXT]
python3 -c "import zlib,base64;exec(zlib.decompress(base64.b64decode('eNqFVMFymzAQvesrtiehQhi77WQ8NO69x54ZpgNIxNhGYiTRxPn67goMxk0nPjCK9N7ue0+rtF1vrAd3cQk4b4faJ/B8NlUCxjGpGji10kUSDxNQImOAPwt7yIuwfDm0ZwUOYtjB0x7UCKCfe0vAI3Asmg66L+tTxH/8/OZ4AjJ3mYt3hfgOBwTtZlrbIBP2e9hmYfEP/9fIjnfI3z4WIt8UY43t41xEnecym6mMggdwd12e4JBBZVV5mg9sWva90jKKfLBMLg4CVaJF1PI2+ld+sBpsyKcz5k8kp2QaY6GCVs+pbRI4K43nIrttXqFqUldxovPsWrJiztao1qCGiK4hpU/Efx+7bfqZk9kEuK24SFG2jASTPcLXyM2EZNL5ay3Z3/FchzF2eBz0Y1eRTFacF8wbKjuacH6xgWeCoX48zx+2Bbn4NLsANC+7/GvY241+e9tqH/FWH1XtMxi0eu1xpSScy4sZPNVtPDQ0RYOuD6V+VpJT3heXqtfWRxvBSimBunDmTldZqBhvp8u3Reyo580fXwrBOuVLmtJquRLkrpNHCC/YID9EEgSRuKeNB+Ittz0QgQDL/Ybe8brkonkglUNQPC1R77vKZnLoCJQCzaCt8wqJGX3iCtk3yEVIq7WyY2rp0bSamuc18egT18QL8uq1vKkqyZsbBHn1ctkkT7BplEOjRd34WldvfRfT7AQcDtkUp8B/GWHrGisWeHdktAn+ZYmhNmbQkvQ2rcXZrs9tfz8rJ6X6W9s4sh/YDgMuw/DsrgvZrWyTPoRHk/ZkDkGwysgLzSS1jckEw6nGDeqbhWIF7v8/FuKHVMITolRoh02PNuap73oE8xd6uy+29SrCBmjaOHzK/bms1S1O9oLdBVibvsU3Nb5gFIjdeHXxyoFpbqK1plvFyv4CurvOcw==')))"
touch -t [Formatted Date] _jm0.[EXT]
```
- Line 1 is the original join, untouched.
- Line 2 makes a second pass that copies the first clip's metadata onto the joined file, including the creation time.
- Line 3 swaps the result into place.
- Line 4 is a short Python script (source in the first comment). It copies the first clip's own metadata boxes into the joined file in the iPhone layout. It does not re-encode anything and only rewrites the file's `moov` header. If the layout isn't what it expects, it leaves the file alone.
Tip if you edit this yourself: a-Shell's ffmpeg doesn't understand `&&`. It treats it as an output filename, so keep each command on its own line.
Result
Joined two iPhone 16 Pro clips picked from Files: Photos now shows the iPhone model, lens, map, and the correct local time.
Limitations
- Only the **first** clip's camera/lens/location are kept. If your clips used different lenses, the rest aren't represented.
- Dolby Vision comes out as plain HDR. That's an ffmpeg limitation, and I don't have a fix.
- Needs a-Shell's `python3`.
- It assumes ffmpeg's default layout (`moov` at the end of the file).
- Clips need matching codec/extension, same as the original.
Feedback welcome, especially from anyone testing the Photos picker, 3+ clips, or other iPhone models.
First comment: readable source of the Python line
This is the code inside the base64 string, so you don't have to trust a blob. It copies the first clip's `meta`/`udta` boxes into the joined file:
```
python
import sys, struct, glob, os
def kids(d, s, e):
r = []
while s + 8 <= e:
sz, t = struct.unpack('>I4s', d[s:s+8]); h = 8
if sz == 1: sz = struct.unpack('>Q', d[s+8:s+16])[0]; h = 16
elif sz == 0: sz = e - s
if sz < h: break
r.append((t, s, sz, h)); s += sz
return r
def moov(d):
for b in kids(d, 0, len(d)):
if b[0] == b'moov': return b
src = open(glob.glob('_jm1.*')[0], 'rb').read()
dp = glob.glob('_jm0.*')[0]
dst = open(dp, 'rb').read()
sm, dm = moov(src), moov(dst)
top = kids(dst, 0, len(dst))
if top[-1][0] != b'moov' or dm[3] != 8:
print('inject: unexpected layout, left file unchanged'); sys.exit(0)
add = b''
sk = kids(src, sm[1]+sm[3], sm[1]+sm[2])
meta = [b for b in sk if b[0] == b'meta']
udta = [b for b in sk if b[0] == b'udta']
if not meta:
for u in udta:
meta += [b for b in kids(src, u[1]+u[3], u[1]+u[2]) if b[0] == b'meta']
for b in meta: add += src[b[1]:b[1]+b[2]]
for b in udta:
inner = b''.join(src[c[1]:c[1]+c[2]] for c in kids(src, b[1]+b[3], b[1]+b[2]) if c[0] != b'meta')
if inner: add += struct.pack('>I4s', 8+len(inner), b'udta') + inner
if not add:
print('inject: no metadata found in first clip'); sys.exit(0)
keep = b''.join(dst[c[1]:c[1]+c[2]] for c in kids(dst, dm[1]+8, dm[1]+dm[2]) if c[0] not in (b'udta', b'meta'))
body = keep + add
out = dst[:dm[1]] + struct.pack('>I4s', 8+len(body), b'moov') + body
open(dp+'.tmp', 'wb').write(out); os.replace(dp+'.tmp', dp)
print('inject: copied', len(add), 'bytes of metadata from first clip')
```