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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6b624b50d7d91e88873fa873b3313751f8ee3ea6988a13028b1c7caf8e5303f6
4
- data.tar.gz: 94eadc0bc92fd9d041d7470660c7268bbd8041c351b841766eae48beca160313
3
+ metadata.gz: bd59dc4bc498d58c9a1e273b457b013b70feee855ebee133c4e20f5efd82029b
4
+ data.tar.gz: d5049f7642cb825da1edabf7a0ee6e5c57cffab0d284d24b5883ea1129683464
5
5
  SHA512:
6
- metadata.gz: f56d53d4367ae4ca448129b55db642c183ff9b090ad120437e7dd2241b9b47a28d54395694a55f9a44b4c91314ae9d7cd898e84c1205190991d8b080eff0a38b
7
- data.tar.gz: d7e288624ac011b23b00b02c22d7ea641c0dacec91f554665533efc8971b6cf3329f399d97247e72aadacc2efcef9346db5984d5d5320151b1dc433be9946159
6
+ metadata.gz: 1f74519fff7dff3daa045b28131040714854c6c98564be4bd9d56f163ccbf79500ab7f49accc4c916da83fdd271edc81bcb52c0cd508a4add0064a06af57eced
7
+ data.tar.gz: 8a9819a18e2eae0bf5d911a626b5a6c3526fd3e01b77a9d0a71f202352647a46333592713ca06643a23bc819e7958e0de68c6684dcc4a53a43c6ed953cd35d19
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2025-07-21
4
+
5
+ - Big Refactoring, Trees in trees, uniform interface
6
+ - First version to be actually somewhat usable
7
+ - Use wahwah gem for now for metadata extraction
8
+
3
9
  ## [0.1.0] - 2025-07-11
4
10
 
5
11
  - Initial release
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Musictree
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
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
@@ -0,0 +1,2 @@
1
+ [tools]
2
+ ruby = "3.4.2"
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.1.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-13 00:00:00.000000000 Z
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 with the help of ffprobe
85
+ summary: Collect metadata on a directory tree of music files
53
86
  test_files: []