media_probe.go
⎇
Raw
1package service
2
3import (
4 "encoding/json"
5 "fmt"
6 "math"
7 "os"
8 "os/exec"
9 "strconv"
10 "strings"
11
12 "vidarchive/internal/models"
13)
14
15// GetMetadata probes media details for the named file within an item. An empty
16// filename (or one that doesn't match) falls back to the item's primary media
17// file, so the detail view shows metadata for whichever file is selected.
18func (s *LibraryService) GetMetadata(relPath, filename string) (*MediaMetadata, error) {
19 item, err := s.GetByRelPath(relPath)
20 if err != nil {
21 return nil, err
22 }
23
24 var target *models.MediaFile
25 if filename != "" {
26 for i := range item.MediaFiles {
27 if item.MediaFiles[i].Filename == filename {
28 target = &item.MediaFiles[i]
29 break
30 }
31 }
32 }
33 if target == nil {
34 target = primaryMediaFile(item)
35 }
36 if target == nil {
37 return nil, fmt.Errorf("no media file")
38 }
39
40 return s.probeMedia(target.Filepath)
41}
42
43func (s *LibraryService) probeMedia(path string) (*MediaMetadata, error) {
44 info, err := os.Stat(path)
45 if err != nil {
46 return nil, err
47 }
48
49 meta := &MediaMetadata{
50 FileSize: info.Size(),
51 }
52
53 cmd := exec.Command(s.ffprobePath,
54 "-v", "error",
55 "-show_format",
56 "-show_streams",
57 "-of", "json",
58 path,
59 )
60 output, err := cmd.Output()
61 if err != nil {
62 return meta, nil
63 }
64
65 var probe ffprobeOutput
66 if err := json.Unmarshal(output, &probe); err != nil {
67 return meta, nil
68 }
69
70 if probe.Format.FormatName != "" {
71 parts := strings.Split(probe.Format.FormatName, ",")
72 meta.Container = parts[0]
73 }
74
75 for _, stream := range probe.Streams {
76 switch stream.CodecType {
77 case "video":
78 vs := VideoStream{
79 Codec: stream.CodecName,
80 Profile: stream.Profile,
81 Width: stream.Width,
82 Height: stream.Height,
83 FPS: parseFPS(stream.RFrameRate),
84 PixelFormat: stream.PixFmt,
85 Bitrate: formatBitrate(stream.BitRate),
86 }
87 meta.VideoStreams = append(meta.VideoStreams, vs)
88 if stream.Width > 0 && stream.Height > 0 {
89 meta.Resolution = fmt.Sprintf("%dx%d", stream.Width, stream.Height)
90 }
91 case "audio":
92 as := AudioStream{
93 Codec: stream.CodecName,
94 SampleRate: stream.SampleRate,
95 Channels: stream.Channels,
96 ChannelLayout: stream.ChannelLayout,
97 SampleFormat: stream.SampleFmt,
98 Bitrate: formatBitrate(stream.BitRate),
99 Language: stream.Tags.Language,
100 }
101 meta.AudioStreams = append(meta.AudioStreams, as)
102 case "subtitle":
103 ss := SubtitleStream{
104 Codec: stream.CodecName,
105 Language: stream.Tags.Language,
106 Title: stream.Tags.Title,
107 }
108 meta.SubtitleStreams = append(meta.SubtitleStreams, ss)
109 }
110 }
111
112 return meta, nil
113}
114
115func formatBitrate(bitRate string) string {
116 if bitRate == "" {
117 return ""
118 }
119 br, err := strconv.ParseInt(bitRate, 10, 64)
120 if err != nil {
121 return ""
122 }
123 return fmt.Sprintf("%d", br/1000)
124}
125
126func parseFPS(rate string) string {
127 var num, den float64
128 if _, err := fmt.Sscanf(rate, "%f/%f", &num, &den); err != nil || den == 0 {
129 return ""
130 }
131 fps := num / den
132 if fps == math.Trunc(fps) {
133 return fmt.Sprintf("%.0f", fps)
134 }
135 return fmt.Sprintf("%.2f", fps)
136}
137
138type MediaMetadata struct {
139 Container string
140 Resolution string
141 FileSize int64
142 VideoStreams []VideoStream
143 AudioStreams []AudioStream
144 SubtitleStreams []SubtitleStream
145}
146
147type VideoStream struct {
148 Codec string
149 Profile string
150 Width int
151 Height int
152 FPS string
153 PixelFormat string
154 Bitrate string
155}
156
157type AudioStream struct {
158 Codec string
159 SampleRate string
160 Channels int
161 ChannelLayout string
162 SampleFormat string
163 Bitrate string
164 Language string
165}
166
167type SubtitleStream struct {
168 Language string
169 Title string
170 Codec string
171}
172
173type ffprobeOutput struct {
174 Format struct {
175 FormatName string `json:"format_name"`
176 BitRate string `json:"bit_rate"`
177 } `json:"format"`
178 Streams []ffprobeStream `json:"streams"`
179}
180
181type ffprobeStream struct {
182 Index int `json:"index"`
183 CodecName string `json:"codec_name"`
184 CodecType string `json:"codec_type"`
185 Profile string `json:"profile"`
186 Width int `json:"width"`
187 Height int `json:"height"`
188 RFrameRate string `json:"r_frame_rate"`
189 AvgFrameRate string `json:"avg_frame_rate"`
190 PixFmt string `json:"pix_fmt"`
191 SampleRate string `json:"sample_rate"`
192 Channels int `json:"channels"`
193 ChannelLayout string `json:"channel_layout"`
194 SampleFmt string `json:"sample_fmt"`
195 BitRate string `json:"bit_rate"`
196 Tags struct {
197 Language string `json:"language"`
198 Title string `json:"title"`
199 } `json:"tags"`
200}
201