-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtransfer.go
More file actions
164 lines (154 loc) · 8.48 KB
/
Copy pathtransfer.go
File metadata and controls
164 lines (154 loc) · 8.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
package waxlabel
import (
"fmt"
"github.com/colespringer/waxlabel/internal/core"
"github.com/colespringer/waxlabel/waxerr"
)
// PlanTransfer simulates copying this document's canonical metadata (tags,
// pictures, chapters, and synced lyrics) into a file of format dst. It reports what each
// piece would carry, downgrade, or lose without writing or needing a destination file. It
// consults dst's capabilities under the given write options, so an option-dependent
// destination is judged as a real write would be.
//
// A read-only destination format reports everything dropped; an unimplemented
// destination is an error. It does not refuse a read-only destination the way
// [Document.PrepareTransfer] does: there is no destination file here to refuse, and the
// whole point of a format-level simulation is to describe the projection. Use
// PrepareTransfer when you have an actual destination file and want an executable plan
// as well.
func (d *Document) PlanTransfer(dst Format, opts ...WriteOption) (TransferReport, error) {
if d.zero() {
return TransferReport{}, fmt.Errorf("%w: document is not initialized; use ParseFile/Parse", waxerr.ErrInvalidData)
}
codec, ok := core.ForFormat(dst)
if !ok {
return TransferReport{}, fmt.Errorf("%w: %s", waxerr.ErrUnsupportedFormat, dst)
}
// nil destination file: PlanTransfer is a pure simulation against the format,
// so the codec answers file-agnostically (any per-file constraint, like the
// WebM cover refusal, is judged when PrepareTransfer/copy supply a real file).
caps := codec.Capabilities(nil, resolveWriteOptions(opts))
return TransferReport{
Source: d.media.Format,
Dest: dst,
Items: core.ProjectTransfer(d.media, caps),
}, nil
}
// PrepareTransfer projects this document's canonical metadata onto dst and
// resolves the result into a ready-to-execute [Plan] that writes dst, returning
// the plan together with the [TransferReport] describing the projection. The
// report is computed from the same projection the plan applies: every carried or
// downgraded item is set on the destination edit, and every dropped item is left off.
//
// The report grades the destination's representational capability per
// field/picture/chapter, including hard structural limits it models (such as the
// MP4 chapter-count cap, reported as a drop). A few codec validity checks that
// depend on the bytes themselves - an embedded image in a format the destination
// cannot label, or a structurally invalid picture set - are enforced when the plan
// is prepared and surface as an error from this call rather than as a per-item
// drop; in that case the returned report still describes the attempted projection.
//
// The transfer overlays src onto dst: each canonical key present in the source
// replaces that key in the destination, the source's pictures replace the destination
// picture set whenever at least one source picture is representable in the destination
// (a source whose covers are all unrepresentable leaves the destination's own covers
// intact), and likewise for chapters and synced lyrics. Destination keys the source does
// not carry are kept. dst is not modified; only [Plan.Execute] writes.
func (d *Document) PrepareTransfer(dst *Document, opts ...WriteOption) (*Plan, TransferReport, error) {
if d.zero() || dst.zero() {
return nil, TransferReport{}, fmt.Errorf("%w: document is not initialized; use ParseFile/Parse", waxerr.ErrInvalidData)
}
caps := dst.Capabilities(opts...)
items := core.ProjectTransfer(d.media, caps)
report := TransferReport{Source: d.media.Format, Dest: dst.media.Format, Items: items}
// A read-only destination that had something to store is a refused write, not a
// clean run of per-item drops. Without this the transfer sets nothing on the editor,
// the codec's no-op fast path returns a NoOpPlan, and the codec's own refusal is
// never reached - so a copy onto a WMA reports every field dropped and then exits 0,
// while the same edit through set exits 3.
//
// Gated on a dropped item, not on ReadOnly alone: a transfer with nothing to carry, or
// one whose every value the destination already holds, writes nothing and legitimately
// succeeds - refusing those would make copy stricter than set on the same file, which
// is the inconsistency this fixes. The error is the codec's own, so ASF keeps
// unsupported-format and a fragmented MP4 keeps unsupported-fragmentation.
if caps.ReadOnly && report.HasDropped() {
return nil, report, readOnlyRefusal(caps)
}
ed := dst.Edit()
// The whole transfer is a faithful carry from the source, not a user-authored
// edit, so suppress the edit-time sanity warnings (chapter past-duration/duplicate,
// single-valued-multi): a copy must not flag metadata the user authored none of.
ed.carried = true
// Pictures are a set. Build the representable subset first, then replace the
// destination's set only when the source has at least one picture the destination can
// write. Clearing before that check would destroy a valid destination cover when every
// source picture is unrepresentable, such as GIF or WebP copied onto an MP4 that
// already has a PNG cover. Representable is the same per-MIME test ProjectTransfer
// uses for picture report items.
//
// The block is also gated on the destination actually storing pictures: a read-only
// format or a no-cover container like WebM cannot hold covers, so touching its picture
// set would only mark a change the writer refuses. Either way, leaving the set
// untouched lets tags transfer while each source cover is reported Dropped.
if !caps.ReadOnly && caps.Pictures.Write != core.AccessNone {
// PartitionRepresentable is the same per-image split ProjectTransfer's report and the
// editor's drop path use, so the write filter cannot drift from what the report grades.
// PartitionPictureSlots then applies the destination's slot selection (APE's two cover
// names), so a picture the report graded Dropped for want of a slot is not handed to
// the writer to drop again.
representable, _, _ := core.PartitionRepresentable(caps.Pictures, core.ClonePictures(d.media.Pictures))
representable, _, _ = core.PartitionPictureSlots(caps.Pictures, representable)
if len(representable) > 0 {
ed.ClearPictures()
for _, p := range representable {
ed.AddPicture(p)
}
}
}
for _, it := range items {
// Dropped means the destination cannot store it. Excluded means policy keeps the
// destination's own value. Neither is written. A slashed track/disc number arrives
// already split into its number and total keys by the read path, so each is graded and
// written independently here - no transfer-time slash handling is needed.
if it.Disposition == Dropped || it.Disposition == Excluded {
continue
}
switch it.Kind {
case core.TransferField:
if vals, ok := d.media.Tags.Get(it.Key); ok {
ed.Set(it.Key, vals...)
}
case core.TransferChapter:
// core.OpenRunToEOFEnd opens a final chapter that runs to the source's own EOF so the
// destination codec refills it to the DESTINATION's EOF; ProjectTransfer grades the
// same list, so the report describes what this writes. Clone after it: it returns the
// input untouched when there is nothing to open.
ed.SetChapters(core.CloneChapters(
core.OpenRunToEOFEnd(d.media.Chapters, d.media.Properties.Duration()))...)
case core.TransferSyncedLyric:
ed.SetSyncedLyrics(core.CloneSyncedLyrics(d.media.SyncedLyrics)...)
}
}
// Carry the source's already-embedded pictures verbatim: ProjectTransfer already
// graded them by the destination's capability, so an exotic-but-valid embedded
// cover (HEIC/AVIF/JXL, which the header sniff rejects by design) must keep
// carrying - copy has no --force to wave it through. Opt the added-picture
// validation out on a fresh slice so the caller's opts are not mutated; no other
// option toggles AllowUnrecognizedPictures, so prepending is order-safe.
plan, err := ed.Prepare(append([]WriteOption{WithUnrecognizedPictures()}, opts...)...)
if err != nil {
return nil, report, err
}
return plan, report, nil
}
// readOnlyRefusal returns the error to fail a transfer onto a read-only destination
// with: the codec's own refusal when it attached one, and a generic unsupported-format
// error for the [Capabilities] fallbacks that carry none (an unknown or unimplemented
// format, which has no codec to ask).
func readOnlyRefusal(caps Capabilities) error {
if err := caps.ReadOnlyReason(); err != nil {
return err
}
return fmt.Errorf("%w: %s files cannot be written", waxerr.ErrUnsupportedFormat, caps.Format)
}