File: tree-from-tags.rb

package info (click to toggle)
bongo 1.1-6
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,476 kB
  • sloc: lisp: 9,550; ruby: 181; makefile: 4
file content (240 lines) | stat: -rwxr-xr-x 6,669 bytes parent folder | download | duplicates (3)
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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
#!/usr/bin/env ruby
## tree-from-tags.rb --- create file hierarchies from media tags
# Copyright (C) 2006, 2007  Daniel Brockman

# Author: Daniel Brockman <daniel@brockman.se>
# Created: April 24, 2006

# This file is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of
# the License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public
# License along with GNU Emacs; if not, write to the Free
# Software Foundation, 51 Franklin Street, Fifth Floor,
# Boston, MA 02110-1301, USA.
#
# To run this program, you need Ruby-taglib, available at
# <https://robinst.github.io/taglib-ruby/>.

require "fileutils"
require "find"
require "taglib"

if ARGV.empty? or ["-?", "-h", "-help", "--help"].include? ARGV.first
  puts "Usage: #$0 [--hardlinks] DIRECTORIES..."
  puts "
This program recursively scans DIRECTORIES for media files in formats
that support embedded file tags, such as Ogg and MP3.

For each file with sufficient embedded information, it creates a symlink
in the current directory, or in a subdirectory of the current directory,
pointing to the original file.

If given the `--hardlinks' option, it creates hardlinks instead.

The symlinks or hardlinks created by this program follow a certain
naming scheme that is understood by Bongo, the Emacs media player."
  exit
end

if ["--hard", "--hardlink", "--hardlinks"].include? ARGV.first
  ARGV.shift
  $hardlinks = true
else
  $hardlinks = false
end

class NotEnoughData < RuntimeError ; end

class String
  def blank? ; !self[/\S/] end
  def trim ; sub(/^\s+|\s+$/, "") end
end

def escape_component (component)
  case component
  when "." then "Dot"
  when ".." then "Double Dot"
  else component.gsub(" - ", " -- ").gsub("/", "\\")
  end
end

def join_components (*components)
  components.compact * " - "
end

def parse_data (data)
  an = data[:artist_name]
  ay = data[:album_year]
  at = data[:album_title]
  ti = data[:track_index]
  tt = data[:track_title]

  ay = nil if ay == "0"

  case
  when an && ay && at && ti && tt
    components = [[an], [ay, at], [ti, tt]]
  when an && ay && at && tt
    components = [[an], [ay, at], [tt]]
  when an && at && ti && tt
    components = [[an], [at], [ti, tt]]
  when an && at && tt
    components = [[an], [at], [tt]]
  when an && tt
    components = [[an], [tt]]
  when tt
    components = [[tt]]
  else raise NotEnoughData
  end

  return components.map { |x| x.map { |x| escape_component(x) } }
end

COLUMNS = ENV["COLUMNS"] || 80

def singleton (&body)
  object = Object.new
  object.extend(Module.new(&body))
  object.send :initialize
  return object
end

status_line = singleton do
  attr_reader :width
  def initialize
    @width = 0
  end

  def remaining_width
    COLUMNS - @width
  end
  
  def clear
    print "\b" * COLUMNS
    print " " * COLUMNS
    print "\b" * COLUMNS
    @width = 0
  end

  def update
    clear ; yield ; flush
  end

  def flush
    $stdout.flush
  end
  
  def << string
    count = [remaining_width, string.size].min
    print string[0 ... count]
    @width += count
  end
end

n_total_files = 0
print "Counting files..." ; $stdout.flush
Find.find(*ARGV) { |x| n_total_files += 1 if FileTest.file? x }
puts " #{n_total_files}."

def warn_skip (file_name, message)
  puts "Warning: Skipping file `#{file_name}': #{message}"
end

n_completed_files = 0           # This counts all files.
n_processed_files = 0           # This only counts recognized files.
n_created_links = 0
Find.find *ARGV do |file_name|
  if FileTest.directory? file_name
    status_line.update do
      percent_done = n_completed_files * 100.0 / n_total_files
      status_line << "[%.2f%%] " % percent_done
      count = status_line.remaining_width - "Processing `'...".size
      if file_name.size > count
        file_name_tail = "[...]" + file_name[-count + 5 .. -1]
      else
        file_name_tail = file_name
      end
      status_line << "Processing `#{file_name_tail}'..."
    end
  elsif FileTest.file? file_name
    next if [".jpg", ".jpeg", ".png", ".gif"].include? \
      File.extname(file_name).downcase
    begin

      TagLib::FileRef.open(file_name) do |file|
        n_processed_files += 1
        unless file.null?
          tag = file.tag
          data = { :artist_name => tag.artist,
                   :album_year  => tag.year.to_s,
                   :album_title => tag.album,
                   :track_index => "#{0 if tag.track < 10}#{tag.track}",
                   :track_title => tag.title }

          for key, value in data do
            if (value.nil? or value.blank?)
              data.delete key
            else
              data[key] = value.trim
            end
          end

          components = parse_data(data).map { |x| join_components(*x) }.
                         inject([]) { |a, x| a << join_components(a.last, x) }

          dir_name = components[0...-1] * "/"
          new_file_name = components * "/" + File.extname(file_name)

          FileUtils.mkdir_p(dir_name) if components.length > 1

          begin
            if $hardlinks
              FileUtils.ln(file_name, new_file_name)
            else
              FileUtils.ln_s(file_name, new_file_name)
            end
            n_created_links += 1
          rescue Errno::EEXIST
            if $hardlinks
              raise unless File.stat(file_name).ino ==
                           File.stat(new_file_name).ino
            else
              raise unless FileTest.symlink? new_file_name and
                File.readlink(new_file_name) == file_name
            end
          end
        end
      end

    rescue NotEnoughData
      puts ; warn_skip file_name, "Not enough track data " +
        "(need at least the track title)."
    rescue Errno::EEXIST
      puts ; warn_skip file_name,
        "Cannot create #{$hardlinks ? "hardlink" : "symbolic link"}: " +
        "Conflicting file already exists. [original error: #$!]"
    rescue Interrupt
      puts ; puts "Interrupted." ; exit(1)
    rescue Exception
      puts ; raise
    end

    n_completed_files += 1
  end
end

status_line.update do
  status_line << "[100%] Processing `#{ARGV.last}'..."
end

puts ; puts "Processed #{n_processed_files} media files " +
  "(created #{n_created_links} " +
  "#{$hardlinks ? "hardlinks" : "symbolic links"})."