summaryrefslogtreecommitdiff
path: root/yazi/plugins/preview-audio.yazi/main.lua
blob: 5703e0a7fddfae309f621db2814f59486bfd5da4 (plain)
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
--- @since 25.12.29

local M = {}

local audio_ffprobe = function(file)
  -- stylua: ignore
  local cmd = Command('ffprobe'):arg {
    '-v', 'quiet',
    '-show_entries', 'stream_tags:format:stream',
    '-of', 'json=c=1',
    file.name
  }

  local output, err = cmd:output()
  if not output then
    return {}, Err('Failed to start `ffprobe`, error: %s', err)
  end

  local json = ya.json_decode(output.stdout)
  if not json then
    return nil, Err('Failed to decode `ffprobe` output: %s', output.stdout)
  elseif type(json) ~= 'table' then
    return nil, Err('Invalid `ffprobe` output: %s', output.stdout)
  end
  ya.dbg(json)

  local audio_stream = json.streams[1]
  local tags = json.format.tags or audio_stream.tags or audio_stream
  local duration = json.format.duration
  if duration then
    duration = tonumber(duration)
    duration = string.format('%d:%02d', math.floor(duration / 60), math.floor(duration % 60))
  end

  local data = {}
  local title, album, aar, ar =
    tags.TITLE or tags.title or '',
    tags.ALBUM or tags.album or '',
    tags.ALBUM_ARTIST or tags.album_artist or '',
    tags.ARTIST or tags.artist or ''

  if title .. album .. ar .. aar ~= '' then
    local img_stream = json.streams[2]
    local date = tags.DATE or tags.date or ''
    local c = ''
    local artist = ar

    if tags.ORIGINALDATE and tags.ORIGINALDATE ~= '' then
      date = date .. ' / ' .. tags.ORIGINALDATE
    end
    if (aar ~= '') and (aar ~= ar) then
      artist = artist .. ' / ' .. aar
    end
    if img_stream then
      c = img_stream.codec_name .. ' ' .. img_stream.width .. 'x' .. img_stream.height
    end

    data = {
      ui.Line(string.format('%s - %s', artist, title)),
      ui.Line(string.format('%s: %s', 'Duration', duration)),
      ui.Line(string.format('%s: %s', 'Album', album)),
      ui.Line(string.format('%s: %s', 'Genre', tags.GENRE or tags.genre or 'No genre')),
      ui.Line(string.format('%s: %s', 'Date', date)),
      c ~= '' and ui.Line(string.format('%s: %s', 'Cover art', c)) or nil,
    }
  end

  local bd = audio_stream.bits_per_raw_sample or '1'
  local sr = audio_stream.sample_rate
  if sr then
    sr = string.format('%.1fkhz', sr / 1000)
  end
  local br = tonumber((audio_stream.bit_rate or json.format.bit_rate or 0) // 1000) .. ' kb/s'
  ya.dbg(bd, sr)
  for _, item in ipairs({
    '',
    '# Specs',
    string.format('%s: %s', 'Format', json.format.format_name),
    string.format('%s: %sbit / %s', 'Quality', bd, sr),
    string.format('%s: %s', 'BitRate', br),
    string.format('%s: %s', 'Channels', tostring(audio_stream.channels or '?')),
  }) do
    data[#data + 1] = ui.Line(item)
  end

  return data
end

function M:peek(job)
  local start, cache = os.clock(), ya.file_cache(job)
  local ok, err = self:preload(job)

  local img_area
  if cache and fs.cha(cache) then
    ya.sleep(math.max(0, rt.preview.image_delay / 1000 + start - os.clock()))
    img_area, err = ya.image_show(cache, job.area)
  end

  ya.preview_widget(job, {
    ui.Text(audio_ffprobe(job.file)):area(ui.Rect {
      x = job.area.x,
      y = job.area.y + (img_area and img_area.h or 0),
      w = job.area.w,
      h = job.area.h,
    }),
  })
end

function M:seek(job) end

function M:preload(job)
  local cache = ya.file_cache(job)
  if not cache then
    return true
  end

  local cha = fs.cha(cache)
  if cha and cha.len > 0 then
    return true
  end

  -- stylua: ignore
  local output, err = Command('ffmpeg')
    :arg({
      '-hide_banner',
      '-loglevel', 'warning',
      '-i', tostring(job.file.url),
      '-an',
      -- '-vcodec', 'copy',
      string.format('%s.jpg', cache),
    })
    :stderr(Command.PIPED)
    :output()

  if not output then
    return true, Err('Failed to start `ffmpeg`, error: %s', err)
  elseif not output.status.success then
    return true, Err('Failed to get image, stderr: %s', output.stderr)
  end

  local ok, err = os.rename(string.format('%s.jpg', cache), tostring(cache))
  if ok then
    return true
  else
    return true, Err('Failed to rename: %s', err)
  end
end

return M