musictree 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/Rakefile +7 -0
- data/checksums/musictree-0.2.0.gem.sha512 +1 -0
- data/lib/musictree/album.rb +69 -0
- data/lib/musictree/ff_prober.rb +37 -0
- data/lib/musictree/track.rb +43 -0
- data/lib/musictree/tree.rb +57 -0
- data/lib/musictree/version.rb +1 -1
- data/lib/musictree.rb +5 -102
- data/mise.toml +2 -0
- metadata +39 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bd59dc4bc498d58c9a1e273b457b013b70feee855ebee133c4e20f5efd82029b
|
4
|
+
data.tar.gz: d5049f7642cb825da1edabf7a0ee6e5c57cffab0d284d24b5883ea1129683464
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1f74519fff7dff3daa045b28131040714854c6c98564be4bd9d56f163ccbf79500ab7f49accc4c916da83fdd271edc81bcb52c0cd508a4add0064a06af57eced
|
7
|
+
data.tar.gz: 8a9819a18e2eae0bf5d911a626b5a6c3526fd3e01b77a9d0a71f202352647a46333592713ca06643a23bc819e7958e0de68c6684dcc4a53a43c6ed953cd35d19
|
data/CHANGELOG.md
CHANGED
data/Rakefile
CHANGED
@@ -2,9 +2,16 @@
|
|
2
2
|
|
3
3
|
require "bundler/gem_tasks"
|
4
4
|
require "minitest/test_task"
|
5
|
+
require "yard"
|
6
|
+
|
7
|
+
CLEAN.include "doc"
|
5
8
|
|
6
9
|
Minitest::TestTask.create
|
7
10
|
|
8
11
|
require "standard/rake"
|
9
12
|
|
10
13
|
task default: %i[test standard]
|
14
|
+
|
15
|
+
YARD::Rake::YardocTask.new do |t|
|
16
|
+
t.files = ["lib/**/*.rb", "-", "README.md", "LICENSE.txt", "CHANGELOG.md"]
|
17
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
cf193a621cd919014ffa52ae2457f09daf90c5b408a1e4da88d7d207aa1c3d7f6c2ce2602de22e4c2d1890a8ed308b888043636b575cb6de025fcc2e0016e289
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# @author Jan "half/byte" Krutisch
|
4
|
+
module Musictree
|
5
|
+
# Album contains the tracks of an album. It is an Enumerable for the tracks
|
6
|
+
# An Album has a title.
|
7
|
+
class Album
|
8
|
+
include Enumerable
|
9
|
+
|
10
|
+
attr_reader :title
|
11
|
+
|
12
|
+
# Scans in the music files that sit in the album path. (No subdirs)
|
13
|
+
# @param path [String] The path to the album
|
14
|
+
# @return [Album] an album that exposes the tracks as an enumerable
|
15
|
+
def self.scan(path)
|
16
|
+
preliminary_name = File.basename(File.dirname(path))
|
17
|
+
moi = new(path, preliminary_name)
|
18
|
+
moi.scan_album
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(path, preliminary_name)
|
22
|
+
@title = preliminary_name
|
23
|
+
@path = path
|
24
|
+
@tracks = []
|
25
|
+
end
|
26
|
+
|
27
|
+
def each(&block)
|
28
|
+
@tracks.each(&block)
|
29
|
+
end
|
30
|
+
|
31
|
+
def empty?
|
32
|
+
@tracks.empty?
|
33
|
+
end
|
34
|
+
|
35
|
+
def [](idx)
|
36
|
+
@tracks[idx]
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_s
|
40
|
+
@tracks.map(&:to_s)
|
41
|
+
end
|
42
|
+
|
43
|
+
def inspect
|
44
|
+
@tracks.map(&:inspect)
|
45
|
+
end
|
46
|
+
|
47
|
+
# An album is an album
|
48
|
+
def album?
|
49
|
+
true
|
50
|
+
end
|
51
|
+
|
52
|
+
def scan_album
|
53
|
+
files = Dir[File.join(@path, "*.{m3u,wav,flac,mp3,opus,aiff}")]
|
54
|
+
if files.empty?
|
55
|
+
# is it a collection?
|
56
|
+
return Tree.scan(@path)
|
57
|
+
end
|
58
|
+
@tracks = files.map do |path|
|
59
|
+
Track.new(path)
|
60
|
+
end
|
61
|
+
if @tracks.all?(&:track)
|
62
|
+
@tracks = @tracks.sort_by(&:track)
|
63
|
+
end
|
64
|
+
titles = @tracks.map(&:album).uniq
|
65
|
+
@title = titles.first
|
66
|
+
self
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "open3"
|
4
|
+
|
5
|
+
module Musictree
|
6
|
+
class FFProbeError < Error; end
|
7
|
+
|
8
|
+
# Let's leave this in here for now if we need this for exotic cases.
|
9
|
+
class FFProber
|
10
|
+
def initialize(path)
|
11
|
+
@path = path
|
12
|
+
probe
|
13
|
+
end
|
14
|
+
|
15
|
+
def [](key)
|
16
|
+
@metadata[key]
|
17
|
+
end
|
18
|
+
|
19
|
+
def probe
|
20
|
+
@metadata = {}
|
21
|
+
stdout, stderr, status = Open3.capture3("ffprobe #{@path} -hide_banner -show_entries 'format_tags'")
|
22
|
+
matches = stdout.match(/\[FORMAT\]\n(.*)\n\[\/FORMAT\]/m)
|
23
|
+
unless status.success?
|
24
|
+
raise FFProbeError.new("Error running ffprobe: #{stderr}")
|
25
|
+
end
|
26
|
+
if matches.nil?
|
27
|
+
raise FFProbeError.new("No FORMAT block in result: #{[stdout, stderr].join}")
|
28
|
+
end
|
29
|
+
matches[1].lines.each do |line|
|
30
|
+
line_matches = line.match(/^TAG:(?<tag>\w+)=(?<value>.*?)\n/)
|
31
|
+
if line_matches
|
32
|
+
@metadata[line_matches[:tag].downcase] = line_matches[:value]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "wahwah"
|
4
|
+
|
5
|
+
# @author Jan "half/byte" Krutisch
|
6
|
+
module Musictree
|
7
|
+
# A track holds the meta information for one music file
|
8
|
+
class Track
|
9
|
+
attr_reader :title, :artist, :track, :album
|
10
|
+
# Scan a music file for metadata
|
11
|
+
# (The method name is consistent to the other classes but a bit shit)
|
12
|
+
# @param path [String] path to the source music file
|
13
|
+
def self.scan(path)
|
14
|
+
moi = new(path)
|
15
|
+
moi.parse_file
|
16
|
+
moi
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(path)
|
20
|
+
@path = path
|
21
|
+
@title = File.basename(@path) # Let's set a preliminary name
|
22
|
+
parse_file
|
23
|
+
end
|
24
|
+
|
25
|
+
def inspect
|
26
|
+
{
|
27
|
+
path: @path,
|
28
|
+
title: @title,
|
29
|
+
artist: @artist,
|
30
|
+
track: @track_no,
|
31
|
+
album: @album
|
32
|
+
}.inspect
|
33
|
+
end
|
34
|
+
|
35
|
+
def parse_file
|
36
|
+
tag = WahWah.open(@path)
|
37
|
+
@title = tag.title
|
38
|
+
@artist = tag.artist
|
39
|
+
@track = tag.track
|
40
|
+
@album = tag.album
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pathname"
|
4
|
+
# @author Jan "half/byte" Krutisch
|
5
|
+
module Musictree
|
6
|
+
# Tree is the base entry class for Musictree.
|
7
|
+
# Use Tree.scan(path) to scan in a full tree for a given file path
|
8
|
+
# Tree is enumerable and contains either other trees (subdirectories) or albums.
|
9
|
+
class Tree
|
10
|
+
attr_reader :title
|
11
|
+
include Enumerable
|
12
|
+
|
13
|
+
# Scan in a file tree
|
14
|
+
# The Tree exposes Albums or subtrees as an Enumerable
|
15
|
+
# @param path [String] the absolute path to the file tree you want to scan
|
16
|
+
# @return [Musictree::Tree]
|
17
|
+
def self.scan(path)
|
18
|
+
moi = new(path)
|
19
|
+
moi.scan_path
|
20
|
+
moi
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(path)
|
24
|
+
@path = path
|
25
|
+
@children = []
|
26
|
+
end
|
27
|
+
|
28
|
+
def each(&block)
|
29
|
+
@children.each(&block)
|
30
|
+
end
|
31
|
+
|
32
|
+
def empty?
|
33
|
+
@children.empty?
|
34
|
+
end
|
35
|
+
|
36
|
+
def [](idx)
|
37
|
+
@children[idx]
|
38
|
+
end
|
39
|
+
|
40
|
+
# A tree is not an album
|
41
|
+
def album?
|
42
|
+
false
|
43
|
+
end
|
44
|
+
|
45
|
+
def scan_path
|
46
|
+
@title = File.basename(@path)
|
47
|
+
Dir[File.join(@path, "/*")].each do |file_or_dir|
|
48
|
+
if File.directory?(file_or_dir)
|
49
|
+
# rel = Pathname.new(file_or_dir).relative_path_from(@path)
|
50
|
+
@children << Album.scan(file_or_dir)
|
51
|
+
else
|
52
|
+
raise Error.new("Only Directories expected here")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/musictree/version.rb
CHANGED
data/lib/musictree.rb
CHANGED
@@ -1,107 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "musictree/version"
|
4
|
-
require "pathname"
|
5
|
-
require "open3"
|
6
|
-
|
7
3
|
module Musictree
|
8
4
|
class Error < StandardError; end
|
9
|
-
|
10
|
-
class FFProbeError < Error; end
|
11
|
-
|
12
|
-
class FFProber
|
13
|
-
def initialize(path)
|
14
|
-
@path = path
|
15
|
-
probe
|
16
|
-
end
|
17
|
-
|
18
|
-
def [](key)
|
19
|
-
@metadata[key]
|
20
|
-
end
|
21
|
-
|
22
|
-
def probe
|
23
|
-
@metadata = {}
|
24
|
-
stdout, stderr, status = Open3.capture3("ffprobe #{@path} -hide_banner -show_entries 'format_tags'")
|
25
|
-
matches = stdout.match(/\[FORMAT\]\n(.*)\n\[\/FORMAT\]/m)
|
26
|
-
unless status.success?
|
27
|
-
raise FFProbeError.new("Error running ffprobe: #{stderr}")
|
28
|
-
end
|
29
|
-
if matches.nil?
|
30
|
-
raise FFProbeError.new("No FORMAT block in result: #{[stdout, stderr].join}")
|
31
|
-
end
|
32
|
-
matches[1].lines.each do |line|
|
33
|
-
line_matches = line.match(/^TAG:(?<tag>\w+)=(?<value>.*?)\n/)
|
34
|
-
if line_matches
|
35
|
-
@metadata[line_matches[:tag]] = line_matches[:value]
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
class Track
|
42
|
-
attr_reader :title, :artist, :track_no, :album
|
43
|
-
def initialize(path)
|
44
|
-
@path = path
|
45
|
-
@title = File.basename(@path) # Let's set a preliminary name
|
46
|
-
parse_file
|
47
|
-
end
|
48
|
-
|
49
|
-
def parse_file
|
50
|
-
prober = FFProber.new(@path)
|
51
|
-
@title = prober["title"]
|
52
|
-
@artist = prober["artist"]
|
53
|
-
@track_no = prober["track"].to_i
|
54
|
-
@album = prober["album"]
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
class Album
|
59
|
-
attr_reader :title, :year, :tracks
|
60
|
-
def initialize(path, preliminary_name)
|
61
|
-
@title = preliminary_name
|
62
|
-
@path = path
|
63
|
-
@tracks = []
|
64
|
-
scan_album
|
65
|
-
end
|
66
|
-
|
67
|
-
def scan_album
|
68
|
-
files = Dir[File.join(@path, "/*.{m3u,wav,flac,mp3,opus,aiff}")]
|
69
|
-
@tracks = files.map do |path|
|
70
|
-
Track.new(path)
|
71
|
-
end
|
72
|
-
if @tracks.all?(&:track_no)
|
73
|
-
@tracks.sort_by(&:track_no)
|
74
|
-
end
|
75
|
-
titles = @tracks.map(&:album).uniq
|
76
|
-
@title = titles.first
|
77
|
-
end
|
78
|
-
|
79
|
-
def album?
|
80
|
-
true
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
class Tree
|
85
|
-
def initialize(path)
|
86
|
-
@path = path
|
87
|
-
@tree = {}
|
88
|
-
scan_path
|
89
|
-
end
|
90
|
-
|
91
|
-
def [](key)
|
92
|
-
@tree[key]
|
93
|
-
end
|
94
|
-
|
95
|
-
def scan_path
|
96
|
-
Dir[File.join(@path, "/*")].each do |file_or_dir|
|
97
|
-
if File.directory?(file_or_dir)
|
98
|
-
rel = Pathname.new(file_or_dir).relative_path_from(@path)
|
99
|
-
@tree[rel.to_s] = Album.new(file_or_dir, File.basename(rel))
|
100
|
-
|
101
|
-
else
|
102
|
-
raise Error.new("Only Directories expected here")
|
103
|
-
end
|
104
|
-
end
|
105
|
-
end
|
106
|
-
end
|
107
5
|
end
|
6
|
+
|
7
|
+
require_relative "musictree/version"
|
8
|
+
require_relative "musictree/album"
|
9
|
+
require_relative "musictree/track"
|
10
|
+
require_relative "musictree/tree"
|
data/mise.toml
ADDED
metadata
CHANGED
@@ -1,14 +1,42 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: musictree
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jan Krutisch
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-07-
|
11
|
-
dependencies:
|
10
|
+
date: 2025-07-21 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: wahwah
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '1.6'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '1.6'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: yard
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 0.9.37
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 0.9.37
|
12
40
|
email:
|
13
41
|
- jan@krutisch.de
|
14
42
|
executables: []
|
@@ -21,8 +49,14 @@ files:
|
|
21
49
|
- LICENSE.txt
|
22
50
|
- README.md
|
23
51
|
- Rakefile
|
52
|
+
- checksums/musictree-0.2.0.gem.sha512
|
24
53
|
- lib/musictree.rb
|
54
|
+
- lib/musictree/album.rb
|
55
|
+
- lib/musictree/ff_prober.rb
|
56
|
+
- lib/musictree/track.rb
|
57
|
+
- lib/musictree/tree.rb
|
25
58
|
- lib/musictree/version.rb
|
59
|
+
- mise.toml
|
26
60
|
- sig/musictree.rbs
|
27
61
|
homepage: https://codeberg.org/bandwebpage/musictree
|
28
62
|
licenses:
|
@@ -45,9 +79,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
45
79
|
- - ">="
|
46
80
|
- !ruby/object:Gem::Version
|
47
81
|
version: '0'
|
48
|
-
requirements:
|
49
|
-
- ffprobe, tested with 7.1.1 and newer
|
82
|
+
requirements: []
|
50
83
|
rubygems_version: 3.6.2
|
51
84
|
specification_version: 4
|
52
|
-
summary: Collect metadata on a directory tree of music files
|
85
|
+
summary: Collect metadata on a directory tree of music files
|
53
86
|
test_files: []
|