From 1fa09b48695cda6a48886885e44d837e5c4b4113 Mon Sep 17 00:00:00 2001 From: iainb Date: Thu, 8 Mar 2018 11:58:15 +0000 Subject: [PATCH 01/34] Reorganising to current standards. --- Gemfile | 7 ++ bin/spfy | 6 -- exe/spfy | 10 ++ lib/spfy.rb | 211 +++++++++++++++++---------------------- lib/spfy/optionreader.rb | 158 +++++++++++++---------------- lib/spfy/version.rb | 5 + spfy.gemspec | 51 ++++------ 7 files changed, 202 insertions(+), 246 deletions(-) create mode 100644 Gemfile delete mode 100755 bin/spfy create mode 100755 exe/spfy create mode 100644 lib/spfy/version.rb diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..9d97ea6 --- /dev/null +++ b/Gemfile @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } + +gemspec diff --git a/bin/spfy b/bin/spfy deleted file mode 100755 index 68b06de..0000000 --- a/bin/spfy +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env ruby - -require 'spfy' - -Spfy.parse_args -Spfy.generate_xml diff --git a/exe/spfy b/exe/spfy new file mode 100755 index 0000000..bec605c --- /dev/null +++ b/exe/spfy @@ -0,0 +1,10 @@ +#!/usr/bin/env ruby + +require 'spfy' +require 'docopts' + + +USAGE = "Use `#{File.basename($0)} --help` for available options." + +Spfy.parse_args +Spfy.generate_xml diff --git a/lib/spfy.rb b/lib/spfy.rb index d07104e..e543c60 100755 --- a/lib/spfy.rb +++ b/lib/spfy.rb @@ -1,25 +1,3 @@ -# -# spfy.rb -# Spfy ("spiffy") -# -# Copyright (c) 2012, Marc Ransome -# -# This file is part of Spfy. -# -# Spfy 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 3 of the License, or -# (at your option) any later version. -# -# Spfy 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 Spfy. If not, see . -# - require "spfy/optionreader" require "optparse" require "ostruct" @@ -27,126 +5,125 @@ require "find" require "uri" -class Spfy - - VERSION = "1.0.0" - USAGE = "Use `#{File.basename($0)} --help` for available options." - - @xml_tags = { - :header => "\n"\ - "\n"\ - "\t\n", - :footer => "\t\n\n", - :title_start => "\t\t\t", - :title_end => "\n", - :artist_start => "\t\t\t", - :artist_end => "\n", - :album_start => "\t\t\t", - :album_end => "\n", - :location_start => "\t\t\tfile://", - :location_end => "\n", - :track_start => "\t\t\n", - :track_end => "\t\t\n", - :track_num_start => "\t\t\t", - :track_num_end => "\n" - } +module Spfy + + class Base + @xml_tags = { + :header => "\n"\ + "\n"\ + "\t\n", + :footer => "\t\n\n", + :title_start => "\t\t\t", + :title_end => "\n", + :artist_start => "\t\t\t", + :artist_end => "\n", + :album_start => "\t\t\t", + :album_end => "\n", + :location_start => "\t\t\tfile://", + :location_end => "\n", + :track_start => "\t\t\n", + :track_end => "\t\t\n", + :track_num_start => "\t\t\t", + :track_num_end => "\n" + } - def self.parse_args - begin - if ARGV.empty? then - exit_with_banner - end + def self.parse_args + begin + if ARGV.empty? then + exit_with_banner + end - # parse command-line arguments - @options = OptionReader.parse(ARGV) + # parse command-line arguments + @options = OptionReader.parse(ARGV) - # test for zero source paths - if @options.dirs.empty? - exit_with_message("No source path(s) specified.") - end + # test for zero source paths + if @options.dirs.empty? + exit_with_message("No source path(s) specified.") + end - rescue OptionParser::InvalidOption, OptionParser::MissingArgument => error - exit_with_message(error.to_s.capitalize) + rescue OptionParser::InvalidOption, OptionParser::MissingArgument => error + exit_with_message(error.to_s.capitalize) + end end - end - def self.generate_xml - @tracks_processed = 0 + def self.generate_xml + @tracks_processed = 0 - if @options.output.any? - puts "Generating XML..." - capture_stdout - end + if @options.output.any? + puts "Generating XML..." + capture_stdout + end - puts @xml_tags[:header] - @options.dirs.each do |dir| - catch :MaxTracksReached do - begin - Find.find(dir) do |path| - xml_for_path(path) + puts @xml_tags[:header] + @options.dirs.each do |dir| + catch :MaxTracksReached do + begin + Find.find(dir) do |path| + xml_for_path(path) + end + rescue Interrupt + abort("\nCancelled, exiting..") end - rescue Interrupt - abort("\nCancelled, exiting..") end end - end - puts @xml_tags[:footer] + puts @xml_tags[:footer] - $stdout = STDOUT if @options.output.any? - end + $stdout = STDOUT if @options.output.any? + end - def self.xml_for_path(path) - TagLib::FileRef.open(path) do |fileref| - tags = fileref.tag + def self.xml_for_path(path) + TagLib::FileRef.open(path) do |fileref| + tags = fileref.tag - next if tags.nil? # skip files with no tags + next if tags.nil? # skip files with no tags - puts "#{@xml_tags[:track_start]}" - parse_location(path) - parse_tag(tags.title, @options.hide_title, @xml_tags[:title_start], @xml_tags[:title_end]) - parse_tag(tags.artist, @options.hide_artist, @xml_tags[:artist_start], @xml_tags[:artist_end]) - parse_tag(tags.album, @options.hide_album, @xml_tags[:album_start], @xml_tags[:album_end]) - parse_track_num(tags.track) - puts "#{@xml_tags[:track_end]}" + puts "#{@xml_tags[:track_start]}" + parse_location(path) + parse_tag(tags.title, @options.hide_title, @xml_tags[:title_start], @xml_tags[:title_end]) + parse_tag(tags.artist, @options.hide_artist, @xml_tags[:artist_start], @xml_tags[:artist_end]) + parse_tag(tags.album, @options.hide_album, @xml_tags[:album_start], @xml_tags[:album_end]) + parse_track_num(tags.track) + puts "#{@xml_tags[:track_end]}" - @tracks_processed += 1 - throw :MaxTracksReached if @options.tracks_to_process[0].to_i > 0 and @tracks_processed == @options.tracks_to_process[0].to_i + @tracks_processed += 1 + throw :MaxTracksReached if @options.tracks_to_process[0].to_i > 0 and @tracks_processed == @options.tracks_to_process[0].to_i + end end - end - def self.parse_location(path) - if !@options.hide_location - encoded_path = URI.escape(path).sub("%5C", "/") # percent encode string for local path - puts "#{@xml_tags[:location_start]}#{encoded_path}#{@xml_tags[:location_end]}" + def self.parse_location(path) + if !@options.hide_location + encoded_path = URI.escape(path).sub("%5C", "/") # percent encode string for local path + puts "#{@xml_tags[:location_start]}#{encoded_path}#{@xml_tags[:location_end]}" + end end - end - def self.parse_tag(tag, suppress_output, start_xml, end_xml) - if !tag.nil? and !suppress_output - puts "#{start_xml}#{tag}#{end_xml}" + def self.parse_tag(tag, suppress_output, start_xml, end_xml) + if !tag.nil? and !suppress_output + puts "#{start_xml}#{tag}#{end_xml}" + end end - end - def self.parse_track_num(track_num) - if !@options.hide_tracknum and !track_num.nil? - if track_num > 0 - puts "#{@xml_tags[:track_num_start]}#{track_num}#{@xml_tags[:track_num_end]}" + def self.parse_track_num(track_num) + if !@options.hide_tracknum and !track_num.nil? + if track_num > 0 + puts "#{@xml_tags[:track_num_start]}#{track_num}#{@xml_tags[:track_num_end]}" + end end end - end - def self.exit_with_message(message) - puts message if message - exit_with_banner - end + def self.exit_with_message(message) + puts message if message + exit_with_banner + end - def self.exit_with_banner - puts USAGE - exit - end + def self.exit_with_banner + puts USAGE + exit + end - def self.capture_stdout - $stdout = File.open(@options.output[0], "w") + def self.capture_stdout + $stdout = File.open(@options.output[0], "w") + end + private_class_method :xml_for_path, :parse_location, :parse_tag, :parse_track_num, :exit_with_message, :exit_with_banner, :capture_stdout end - private_class_method :xml_for_path, :parse_location, :parse_tag, :parse_track_num, :exit_with_message, :exit_with_banner, :capture_stdout -end +end \ No newline at end of file diff --git a/lib/spfy/optionreader.rb b/lib/spfy/optionreader.rb index 1566bef..6009254 100644 --- a/lib/spfy/optionreader.rb +++ b/lib/spfy/optionreader.rb @@ -1,113 +1,93 @@ -# -# optionreader.rb -# Spfy ("spiffy") -# -# Copyright (c) 2012, Marc Ransome -# -# This file is part of Spfy. -# -# Spfy 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 3 of the License, or -# (at your option) any later version. -# -# Spfy 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 Spfy. If not, see . -# +module Spfy + class OptionReader -class OptionReader - - def self.parse(args) + def self.parse(args) - options = OpenStruct.new - options.output = [] - options.verbose = false - options.hide_title = false - options.hide_artist = false - options.hide_album = false - options.hide_location = false - options.hide_tracknum = false - options.dirs = [] - options.tracks_to_process = [] + options = OpenStruct.new + options.output = [] + options.verbose = false + options.hide_title = false + options.hide_artist = false + options.hide_album = false + options.hide_location = false + options.hide_tracknum = false + options.dirs = [] + options.tracks_to_process = [] - opts = OptionParser.new do |opts| - opts.banner = "Usage: #{File.basename($0)} [options] dir1 ... dirN" + opts = OptionParser.new do |opts| + opts.banner = "Usage: #{File.basename($0)} [options] dir1 ... dirN" - opts.separator "" + opts.separator "" - opts.separator "#{File.basename($0).capitalize} supports multiple directory paths and traverses each directory recursively." - opts.separator "Non-audio files are ignored and will not impact #{File.basename($0)}'s output." + opts.separator "#{File.basename($0).capitalize} supports multiple directory paths and traverses each directory recursively." + opts.separator "Non-audio files are ignored and will not impact #{File.basename($0)}'s output." - opts.separator "" - opts.separator "Output options:" + opts.separator "" + opts.separator "Output options:" - opts.on("-o", "--output FILE", "File to output XSPF data to") do |out| - options.output << out - end + opts.on("-o", "--output FILE", "File to output XSPF data to") do |out| + options.output << out + end - opts.on("-f", "--no-location", "Suppress file location output") do - options.hide_location = true - end + opts.on("-f", "--no-location", "Suppress file location output") do + options.hide_location = true + end - opts.on("-t", "--no-title", "Suppress track title in output") do - options.hide_title = true - end + opts.on("-t", "--no-title", "Suppress track title in output") do + options.hide_title = true + end - opts.on("-a", "--no-artist", "Suppress artist name in output") do - options.hide_artist = true - end + opts.on("-a", "--no-artist", "Suppress artist name in output") do + options.hide_artist = true + end - opts.on("-l", "--no-album", "Suppress album name in output") do - options.hide_album = true - end + opts.on("-l", "--no-album", "Suppress album name in output") do + options.hide_album = true + end - opts.on("-n", "--no-tracknum", "Suppress track number in output") do - options.hide_tracknum = true - end + opts.on("-n", "--no-tracknum", "Suppress track number in output") do + options.hide_tracknum = true + end - opts.on("-m", "--max-tracks NUM", "Limit the output to NUM tracks") do |num| - options.tracks_to_process << num - options.track_limit = true - end + opts.on("-m", "--max-tracks NUM", "Limit the output to NUM tracks") do |num| + options.tracks_to_process << num + options.track_limit = true + end - opts.separator "" - opts.separator "Misc options:" + opts.separator "" + opts.separator "Misc options:" - opts.on("-v", "--version", "Display version information") do - puts "#{File.basename($0).capitalize} #{Spfy::VERSION} Copyright (c) 2012 Marc Ransome " - puts "This program comes with ABSOLUTELY NO WARRANTY, use it at your own risk." - puts "This is free software, and you are welcome to redistribute it under" - puts "certain conditions; see LICENSE.txt for details." - exit - end + opts.on("-v", "--version", "Display version information") do + puts "#{File.basename($0).capitalize} #{Spfy::VERSION} Copyright (c) 2012 Marc Ransome " + puts "This program comes with ABSOLUTELY NO WARRANTY, use it at your own risk." + puts "This is free software, and you are welcome to redistribute it under" + puts "certain conditions; see LICENSE.txt for details." + exit + end - opts.on_tail("-h", "--help", "Show this screen") do - puts opts - exit + opts.on_tail("-h", "--help", "Show this screen") do + puts opts + exit + end end - end - # parse then remove the remaining arguments - opts.parse!(args) + # parse then remove the remaining arguments + opts.parse!(args) - # test leftover input for valid paths - args.each do |dir| + # test leftover input for valid paths + args.each do |dir| - # add path to global dirs variable - if File.directory?(dir) - options.dirs << dir - end + # add path to global dirs variable + if File.directory?(dir) + options.dirs << dir + end - end + end - # return the options array - options + # return the options array + options - end # def self.parse(args) + end # def self.parse(args) -end # class OptionReader + end # class OptionReader +end diff --git a/lib/spfy/version.rb b/lib/spfy/version.rb new file mode 100644 index 0000000..3b3c9b2 --- /dev/null +++ b/lib/spfy/version.rb @@ -0,0 +1,5 @@ +module Spfy + # Library version, follows semver + # @see https://semver.org + VERSION="1.0.0" +end \ No newline at end of file diff --git a/spfy.gemspec b/spfy.gemspec index a2816cb..dddc4d2 100644 --- a/spfy.gemspec +++ b/spfy.gemspec @@ -1,36 +1,19 @@ -# -# spfy.gemspec -# Spfy ("spiffy") -# -# Copyright (c) 2012, Marc Ransome -# -# This file is part of Spfy. -# -# Spfy 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 3 of the License, or -# (at your option) any later version. -# -# Spfy 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 Spfy. If not, see . -# +# -*- encoding: utf-8 -*- +require File.expand_path('../lib/spfy/version', __FILE__) -Gem::Specification.new do |s| - s.name = 'spfy' - s.version = '1.0.0' - s.date = '2013-07-28' - s.summary = 'XSPF playlist generator' - s.description = 'Spfy is a simple command-line tool for generating XSPF playlists from metadata stored in several popular audio formats.' - s.authors = ["Marc Ransome"] - s.email = 'marc.ransome@fidgetbox.co.uk' - s.files = `git ls-files`.split("\n") - s.executables << 'spfy' - s.add_runtime_dependency 'taglib-ruby', '>= 0.5.0' - s.homepage = 'http://marcransome.github.com/spfy' - s.license = 'GPL-3' +Gem::Specification.new do |gem| + gem.name = "spfy" + gem.version = Spfy::VERSION + gem.summary = 'XSPF playlist generator' + gem.description = 'Spfy is a simple command-line tool for generating XSPF playlists from metadata stored in several popular audio formats.' + gem.authors = ["Marc Ransome"] + gem.email = 'marc.ransome@fidgetbox.co.uk' + gem.files = `git ls-files`.split("\n") + gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) + gem.bindir = "exe" + gem.executables = gem.files.grep(%r{^exe/}).map{ |f| File.basename(f) } + gem.add_runtime_dependency 'taglib-ruby', '>= 0.5.0' + gem.add_runtime_dependency "docopt", ">= 0.6.1" + gem.homepage = 'https://github.com/marcransome/Spfy' + gem.license = 'GPL-3' end From f9085126a986915921f9e48ae1f0f334e9001d4d Mon Sep 17 00:00:00 2001 From: iainb Date: Sun, 11 Mar 2018 13:54:11 +0000 Subject: [PATCH 02/34] Ignore more things. It's probably a good motto for life. --- .gitignore | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..22ab228 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +.idea +.project +.loadpath +.DS_Store +pkg +doc +vendor/ +vendor.noindex/ +.bundle/ +Gemfile.lock +bin/ +coverage/ +config/ +backup/ +scratch/ +.yardoc/ +spec/support/fixtures From d602c8cd720a62287bde8204442e65388b28ce3b Mon Sep 17 00:00:00 2001 From: iainb Date: Sun, 11 Mar 2018 14:44:18 +0000 Subject: [PATCH 03/34] Don't ignore everything in fixtures. --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 22ab228..7643d17 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,4 @@ config/ backup/ scratch/ .yardoc/ -spec/support/fixtures +spec/support/fixtures/albums From 55163709f7f4580de617f8443cdca249b09b8d7e Mon Sep 17 00:00:00 2001 From: iainb Date: Sun, 11 Mar 2018 14:45:02 +0000 Subject: [PATCH 04/34] Specs and set up. --- Rakefile | 25 +++ spec/spec_helper.rb | 32 ++++ spec/spfy_spec.rb | 147 ++++++++++++++++++ spec/support/fixtures/all-albums.playlist.xml | 95 +++++++++++ .../support/fixtures/mp3-and-mp4.playlist.xml | 65 ++++++++ spec/support/fixtures/mp3.playlist.xml | 45 ++++++ spec/support/fixtures/mp4.m4a.playlist.xml | 19 +++ spec/support/fixtures/mp4.m4a.xml | 10 ++ 8 files changed, 438 insertions(+) create mode 100644 Rakefile create mode 100644 spec/spec_helper.rb create mode 100644 spec/spfy_spec.rb create mode 100644 spec/support/fixtures/all-albums.playlist.xml create mode 100644 spec/support/fixtures/mp3-and-mp4.playlist.xml create mode 100644 spec/support/fixtures/mp3.playlist.xml create mode 100644 spec/support/fixtures/mp4.m4a.playlist.xml create mode 100644 spec/support/fixtures/mp4.m4a.xml diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..ce8ec1a --- /dev/null +++ b/Rakefile @@ -0,0 +1,25 @@ +require 'pathname' + +desc "Install the spec fixtures" +task :fixtures do + gem_home = Pathname(ENV["GEM_HOME"]) + files = gem_home.join("gems").glob("taglib-ruby-*/test/data/*").reject{|x| x.extname.to_s == ".cpp" } + # mk dir tree + spec_dir = Pathname(__dir__).join("spec/support/fixtures/albums") + spec_dir.mkpath + %w{flac mp3 mp4 oga wav}.each do |ext| + album = spec_dir.join(ext) + album.mkpath + # find taglib and copy files + # And yes, this is a horrible hack + if ext == "mp4" + exts = ["m4a","aiff"] + else + exts = [ext] + end + exts.each do |ext| + FileUtils.cp files.select{|x| x.extname.to_s == ".#{ext}" }, album + end + end + puts "Audio files copied from taglib to spec/support/fixtures/albums" +end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..8d6d9a2 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,32 @@ +require 'simplecov' +require "rspec/its" +require "rspec-given" +require 'timecop' + +SimpleCov.start do + add_filter "/spec/" + add_filter "/coverage/" + add_filter "/vendor.noindex/" +end + +require 'pathname' +Fixtures = Pathname(__dir__).join("support/fixtures") + +if ENV["DEBUG"] + require 'pry-byebug' + require 'pry-state' + binding.pry +end + + +RSpec.configure do |config| + config.expect_with(:rspec) { |c| c.syntax = [:should,:expect] } + + config.before(:each, :time_sensitive => true) do + Timecop.freeze Time.parse "2018-03-11T06:49:16+00:00" + end + + config.after(:each, :time_sensitive => true) do + Timecop.return + end +end \ No newline at end of file diff --git a/spec/spfy_spec.rb b/spec/spfy_spec.rb new file mode 100644 index 0000000..7cda05a --- /dev/null +++ b/spec/spfy_spec.rb @@ -0,0 +1,147 @@ +require 'spec_helper' +require_relative "../lib/spfy.rb" +require 'open3' + +Albums = Fixtures.join("albums") + +# Must use tidy because Haml and ERB cannot be trusted to produce +# the same data each time or between versions +def cmd str + stdout, _= Open3.capture2("tidy -q -i -xml", stdin_data: str) +# puts stdout + stdout +end + +describe "Track" do + context "Most simple case" do + Given(:path) { Albums.join("mp4/mp4.m4a") } + Given(:track) { Spfy::Track.new path } + # Mung the path in the file so it works on any machine + Given(:xml_comparator) { Fixtures.join("mp4.m4a.xml").read.sub /FIXTURE/, Albums.join("mp4/mp4.m4a").to_path } + When(:xml) { track.to_xml } + Then { cmd(xml.chomp) == cmd( xml_comparator.chomp ) } + end +end + +describe "Playlist" do + context "Most simple case", :time_sensitive do + Given(:options) { + { + "--help" => false, + "--output" => nil, + "--title" => nil, + "--creator" => nil, + "--date" => nil, + "--annotation" => nil, + "--no-location" => false, + "--no-title" => false, + "--no-artist" => false, + "--no-album" => false, + "--no-tracknum" => false, + "--max-tracks" => nil, + "PATHS"=>["spec/support/fixtures/albums/mp4/mp4.m4a"], + "--version" => false + } + } + Given(:playlist) { Spfy::Playlist.new( options ) } + Given(:xml_comparator) { + Fixtures.join("mp4.m4a.playlist.xml").read + .sub( /FIXTURE/, Albums.join("mp4/mp4.m4a").to_path ) + .sub( /USER/, ENV["USER"] ) + } + When(:xml) { playlist.to_xml } + Then { cmd(xml.chomp) == cmd(xml_comparator.chomp) } +# Then { playlist.options == {} } +# Then { playlist.paths == [] } +# Then { playlist.creator == "ME" } +# Then { playlist.date == "today" } +# Then { playlist.annotation == "notes" } + end + + context "A directory of files", :time_sensitive do + Given(:options) { + { + "--help" => false, + "--output" => nil, + "--title" => nil, + "--creator" => nil, + "--date" => nil, + "--annotation" => nil, + "--no-location" => false, + "--no-title" => false, + "--no-artist" => false, + "--no-album" => false, + "--no-tracknum" => false, + "--max-tracks" => nil, + "PATHS"=>["spec/support/fixtures/albums/mp3"], + "--version" => false + } + } + Given(:playlist) { Spfy::Playlist.new( options ) } + Given(:xml_comparator) { + Fixtures.join("mp3.playlist.xml").read + .gsub( /ALBUMS/, Albums.to_path ) + .sub( /USER/, ENV["USER"] ) + } + When(:xml) { playlist.to_xml } + Then { cmd(xml.chomp) == cmd(xml_comparator.chomp) } + end + + context "A directory of directories of files", :time_sensitive do + Given(:options) { + { + "--help" => false, + "--output" => nil, + "--title" => nil, + "--creator" => nil, + "--date" => nil, + "--annotation" => nil, + "--no-location" => false, + "--no-title" => false, + "--no-artist" => false, + "--no-album" => false, + "--no-tracknum" => false, + "--max-tracks" => nil, + "PATHS"=>["spec/support/fixtures/albums"], + "--version" => false + } + } + Given(:playlist) { Spfy::Playlist.new( options ) } + Given(:xml_comparator) { + Fixtures.join("all-albums.playlist.xml").read + .gsub( /ALBUMS/, Albums.to_path ) + .sub( /USER/, ENV["USER"] ) + } + When(:xml) { playlist.to_xml } + Then { cmd(xml.chomp) == cmd(xml_comparator.chomp) } + end + + context "Multiple locations", :time_sensitive do + Given(:options) { + { + "--help" => false, + "--output" => nil, + "--title" => nil, + "--creator" => nil, + "--date" => nil, + "--annotation" => nil, + "--no-location" => false, + "--no-title" => false, + "--no-artist" => false, + "--no-album" => false, + "--no-tracknum" => false, + "--max-tracks" => nil, + "PATHS"=>["spec/support/fixtures/albums/mp3", "spec/support/fixtures/albums/mp4"], + "--version" => false + } + } + Given(:playlist) { Spfy::Playlist.new( options ) } + Given(:xml_comparator) { + Fixtures.join("mp3-and-mp4.playlist.xml").read + .gsub( /ALBUMS/, Albums.to_path ) + .sub( /USER/, ENV["USER"] ) + } + When(:xml) { playlist.to_xml } + Then { cmd(xml.chomp) == cmd(xml_comparator.chomp) } + end +end \ No newline at end of file diff --git a/spec/support/fixtures/all-albums.playlist.xml b/spec/support/fixtures/all-albums.playlist.xml new file mode 100644 index 0000000..f4147e3 --- /dev/null +++ b/spec/support/fixtures/all-albums.playlist.xml @@ -0,0 +1,95 @@ + + + albums + USER + 2018-03-11T06:49:16+00:00 + Created with Spfy.rb + + + ALBUMS/flac/flac.flac + Album + Artist + Test file + Pop + Title + 7 + 2011 + + + ALBUMS/mp3/crash.mp3 + 0 + 0 + + + ALBUMS/mp3/id3v1.mp3 + Album + Artist + Comment + Pop + Title + 7 + 2011 + + + ALBUMS/mp3/relative-volume.mp3 + 0 + 0 + + + ALBUMS/mp3/sample.mp3 + Dummy Album + Dummy Artist + Dummy Comment + Pop + Dummy Title + 1 + 2000 + + + ALBUMS/mp3/unicode.mp3 + 你好 + 0 + 0 + + + ALBUMS/mp4/aiff-sample.aiff + AIFF Dummy Album Title + AIFF Dummy Artist Name + AIFF Dummy Comment + Jazz + AIFF Dummy Track Title - ID3v2.4 + 3 + 2014 + + + ALBUMS/mp4/mp4.m4a + Album + Artist + Comment + Pop + Title + 7 + 2011 + + + ALBUMS/oga/vorbis.oga + Album + Artist + Test file + Pop + Title + 7 + 2011 + + + ALBUMS/wav/wav-sample.wav + WAV Dummy Album Title + WAV Dummy Artist Name + WAV Dummy Comment + Jazz + WAV Dummy Track Title + 5 + 2014 + + + \ No newline at end of file diff --git a/spec/support/fixtures/mp3-and-mp4.playlist.xml b/spec/support/fixtures/mp3-and-mp4.playlist.xml new file mode 100644 index 0000000..f39b0b4 --- /dev/null +++ b/spec/support/fixtures/mp3-and-mp4.playlist.xml @@ -0,0 +1,65 @@ + + + mp3 + USER + 2018-03-11T06:49:16+00:00 + Created with Spfy.rb + + + ALBUMS/mp3/crash.mp3 + 0 + 0 + + + ALBUMS/mp3/id3v1.mp3 + Album + Artist + Comment + Pop + Title + 7 + 2011 + + + ALBUMS/mp3/relative-volume.mp3 + 0 + 0 + + + ALBUMS/mp3/sample.mp3 + Dummy Album + Dummy Artist + Dummy Comment + Pop + Dummy Title + 1 + 2000 + + + ALBUMS/mp3/unicode.mp3 + 你好 + 0 + 0 + + + ALBUMS/mp4/aiff-sample.aiff + AIFF Dummy Album Title + AIFF Dummy Artist Name + AIFF Dummy Comment + Jazz + AIFF Dummy Track Title - ID3v2.4 + 3 + 2014 + + + ALBUMS/mp4/mp4.m4a + Album + Artist + Comment + Pop + Title + 7 + 2011 + + + \ No newline at end of file diff --git a/spec/support/fixtures/mp3.playlist.xml b/spec/support/fixtures/mp3.playlist.xml new file mode 100644 index 0000000..213ccaa --- /dev/null +++ b/spec/support/fixtures/mp3.playlist.xml @@ -0,0 +1,45 @@ + + + mp3 + USER + 2018-03-11T06:49:16+00:00 + Created with Spfy.rb + + + ALBUMS/mp3/crash.mp3 + 0 + 0 + + + ALBUMS/mp3/id3v1.mp3 + Album + Artist + Comment + Pop + Title + 7 + 2011 + + + ALBUMS/mp3/relative-volume.mp3 + 0 + 0 + + + ALBUMS/mp3/sample.mp3 + Dummy Album + Dummy Artist + Dummy Comment + Pop + Dummy Title + 1 + 2000 + + + ALBUMS/mp3/unicode.mp3 + 你好 + 0 + 0 + + + \ No newline at end of file diff --git a/spec/support/fixtures/mp4.m4a.playlist.xml b/spec/support/fixtures/mp4.m4a.playlist.xml new file mode 100644 index 0000000..bfdbcec --- /dev/null +++ b/spec/support/fixtures/mp4.m4a.playlist.xml @@ -0,0 +1,19 @@ + + + mp4 + USER + 2018-03-11T06:49:16+00:00 + Created with Spfy.rb + + + FIXTURE + Album + Artist + Comment + Pop + Title + 7 + 2011 + + + \ No newline at end of file diff --git a/spec/support/fixtures/mp4.m4a.xml b/spec/support/fixtures/mp4.m4a.xml new file mode 100644 index 0000000..4151662 --- /dev/null +++ b/spec/support/fixtures/mp4.m4a.xml @@ -0,0 +1,10 @@ + + FIXTURE + Album + Artist + Comment + Pop + Title + 7 + 2011 + \ No newline at end of file From 34dde91eb783a451f7cfdb946656d24d89592ddf Mon Sep 17 00:00:00 2001 From: iainb Date: Sun, 11 Mar 2018 14:45:38 +0000 Subject: [PATCH 05/34] All the things I like to have for development. --- Gemfile | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Gemfile b/Gemfile index 9d97ea6..e900703 100644 --- a/Gemfile +++ b/Gemfile @@ -5,3 +5,20 @@ source "https://rubygems.org" git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } gemspec + +group :development do + gem "rake" + gem "pry-byebug" + gem "pry-state" + gem "rb-readline" + gem "awesome_print" +end + + +group :test do + gem "rspec" + gem "rspec-its" + gem "simplecov" + gem "rspec" + gem "rspec-given" +end From c5945861a2c6db76cd6e2c808f386676729df581 Mon Sep 17 00:00:00 2001 From: iainb Date: Sun, 11 Mar 2018 14:46:41 +0000 Subject: [PATCH 06/34] Reorganisation and rewrite. Used docopts for the optparsing, it makes things so easy. Moved the logic from printing out the elements in code to using templates, bit more flexible and generally easier to deal with. --- exe/spfy | 41 ++++- lib/spfy.rb | 263 ++++++++++++++++++++------------- lib/templates/playlist.xml.erb | 10 ++ lib/templates/track.xml.erb | 3 + 4 files changed, 208 insertions(+), 109 deletions(-) create mode 100644 lib/templates/playlist.xml.erb create mode 100644 lib/templates/track.xml.erb diff --git a/exe/spfy b/exe/spfy index bec605c..b4c5996 100755 --- a/exe/spfy +++ b/exe/spfy @@ -1,10 +1,43 @@ #!/usr/bin/env ruby require 'spfy' -require 'docopts' +require 'docopt' +doc = < e + puts e.message +end + +playlist = Spfy::Playlist.new options + +blk = ->(f){ f.puts playlist.to_xml } + +if options["--output"] + File.open(options["--output"], "w", &blk) +else + blk.call(Kernel) +end \ No newline at end of file diff --git a/lib/spfy.rb b/lib/spfy.rb index e543c60..d4bfee1 100755 --- a/lib/spfy.rb +++ b/lib/spfy.rb @@ -1,129 +1,182 @@ -require "spfy/optionreader" -require "optparse" -require "ostruct" require "taglib" -require "find" -require "uri" +require 'tilt' +require 'pathname' +require 'time' +# Create XSPF playlist files module Spfy - class Base - @xml_tags = { - :header => "\n"\ - "\n"\ - "\t\n", - :footer => "\t\n\n", - :title_start => "\t\t\t", - :title_end => "\n", - :artist_start => "\t\t\t", - :artist_end => "\n", - :album_start => "\t\t\t", - :album_end => "\n", - :location_start => "\t\t\tfile://", - :location_end => "\n", - :track_start => "\t\t\n", - :track_end => "\t\t\n", - :track_num_start => "\t\t\t", - :track_num_end => "\n" - } - - def self.parse_args - begin - if ARGV.empty? then - exit_with_banner - end - - # parse command-line arguments - @options = OptionReader.parse(ARGV) - - # test for zero source paths - if @options.dirs.empty? - exit_with_message("No source path(s) specified.") + # Locations of the ERB templates + TEMPLATES = Pathname(__dir__).join("templates") + + # Produces the track entity + # @see http://xspf.org/xspf-v1.html#rfc.section.4.1.1.2.14.1.1 + class Track + def initialize path, options: {} + @path = Pathname(path).expand_path + @template = Tilt::ERBTemplate.new(TEMPLATES.join("track.xml.erb")) + @processed = false + @available_tags = [:location, :album, :artist, :comment, :genre, :title, :trackNum, :year] + process + end + + # The file path + attr_reader :path + alias_method :location, :path + + # The targeted sub-elements + attr_accessor :album, :artist, :comment, :genre, :title, :trackNum, :year + + + # For a block {|name,tag| ... } + # @yield [name,tag] Yield the element name and its contents. + def each_tag skip_nils=true + return enum_for(:each_tag) unless block_given? + @available_tags.each do |tag| + contents = self.send(tag) + if skip_nils and ( + contents.nil? or + (contents.respond_to?(:empty?) and contents.empty?) + ) + next end - - rescue OptionParser::InvalidOption, OptionParser::MissingArgument => error - exit_with_message(error.to_s.capitalize) + yield tag, contents end end + + + # @private + # Process the options into a track entity. + # Calls TagLib + def process refresh=false + if refresh or @processed == false + TagLib::FileRef.open(@path.to_path) do |fileref| + tags = fileref.tag - def self.generate_xml - @tracks_processed = 0 + next if tags.nil? # skip files with no tags - if @options.output.any? - puts "Generating XML..." - capture_stdout - end - - puts @xml_tags[:header] - @options.dirs.each do |dir| - catch :MaxTracksReached do - begin - Find.find(dir) do |path| - xml_for_path(path) - end - rescue Interrupt - abort("\nCancelled, exiting..") - end + @album = tags.album + @artist = tags.artist + @comment = tags.comment + @genre = tags.genre + @title = tags.title + @trackNum = tags.track + @year = tags.year end + @processed = true end - puts @xml_tags[:footer] - - $stdout = STDOUT if @options.output.any? end - def self.xml_for_path(path) - TagLib::FileRef.open(path) do |fileref| - tags = fileref.tag - - next if tags.nil? # skip files with no tags - - puts "#{@xml_tags[:track_start]}" - parse_location(path) - parse_tag(tags.title, @options.hide_title, @xml_tags[:title_start], @xml_tags[:title_end]) - parse_tag(tags.artist, @options.hide_artist, @xml_tags[:artist_start], @xml_tags[:artist_end]) - parse_tag(tags.album, @options.hide_album, @xml_tags[:album_start], @xml_tags[:album_end]) - parse_track_num(tags.track) - puts "#{@xml_tags[:track_end]}" - - @tracks_processed += 1 - throw :MaxTracksReached if @options.tracks_to_process[0].to_i > 0 and @tracks_processed == @options.tracks_to_process[0].to_i - end + + # The renderer + # @return [String] + def to_xml + process + @template.render(self) end - - def self.parse_location(path) - if !@options.hide_location - encoded_path = URI.escape(path).sub("%5C", "/") # percent encode string for local path - puts "#{@xml_tags[:location_start]}#{encoded_path}#{@xml_tags[:location_end]}" - end + end + + + # Produces the playlist entity + # @see http://xspf.org/xspf-v1.html#rfc.section.4.1.1 + class Playlist + + def initialize options + set_traps_for_signals + @options = options + @files = [] + @template = Tilt::ERBTemplate.new(TEMPLATES.join("playlist.xml.erb")) + parse @options end - - def self.parse_tag(tag, suppress_output, start_xml, end_xml) - if !tag.nil? and !suppress_output - puts "#{start_xml}#{tag}#{end_xml}" - end + + attr_reader :creator, :title, :options, :paths + + # @see http://xspf.org/xspf-v1.html#rfc.section.4.1.1.2.3 + def annotation + @annotation end - - def self.parse_track_num(track_num) - if !@options.hide_tracknum and !track_num.nil? - if track_num > 0 - puts "#{@xml_tags[:track_num_start]}#{track_num}#{@xml_tags[:track_num_end]}" + + + # @see http://xspf.org/xspf-v1.html#rfc.section.4.1.1.2.8 + def date + Time.now.iso8601 + end + + + # @private + # The option parser + # spfy here there and everywhere + # { + # "--help" => false, + # "--output" => nil, + # "--title" => nil, + # "--creator" => nil, + # "--date" => nil, + # "--annotation" => nil, + # "--no-location" => false, + # "--no-title" => false, + # "--no-artist" => false, + # "--no-album" => false, + # "--no-tracknum" => false, + # "--max-tracks" => nil, + # "PATHS"=>["spec/support/fixtures/albums/mp4/mp4.m4a"], + # "--version" => false + # } + def parse options + @paths = (options.fetch "PATHS", []).map{|path| Pathname(path.sub /^~/, ENV["HOME"]) } + return if @paths.empty? + @title = if options["--title"] + options["--title"] + else + if @paths.first.directory? + @paths.first.basename + else + @paths.first.parent.basename end end + @creator = options["--creator"] || ENV["USER"] + @annotation = options["--annotation"] || "Created with Spfy.rb" + @noes = options.select{|k,v| k =~ /^\-\-no\-/ and v } + #@max_tracks = @option["--max-tracks"] end - def self.exit_with_message(message) - puts message if message - exit_with_banner - end - - def self.exit_with_banner - puts USAGE - exit + + # @private + # For interruptions + def set_traps_for_signals + trap(:SIGINT) do + warn " Received Ctrl+c" + # cleanup + exit 0 + end end - - def self.capture_stdout - $stdout = File.open(@options.output[0], "w") + + + # Render the playlist and any tracks. + def to_xml + return "" if @paths.empty? + #catch :MaxTracksReached { + @template.render(self) do + mapped = [] + @paths.each { |path| + if path.directory? + path.find do |pn| + if pn.directory? + pn.basename.to_s[0] == '.' ? + Find.prune : + next + else + mapped << Spfy::Track.new(pn) + end + end + else + mapped << Spfy::Track.new(path) + end + } + mapped.map(&:to_xml).join("\n") + end + #} end - private_class_method :xml_for_path, :parse_location, :parse_tag, :parse_track_num, :exit_with_message, :exit_with_banner, :capture_stdout + end end \ No newline at end of file diff --git a/lib/templates/playlist.xml.erb b/lib/templates/playlist.xml.erb new file mode 100644 index 0000000..17b35c1 --- /dev/null +++ b/lib/templates/playlist.xml.erb @@ -0,0 +1,10 @@ + + + <%= title %> + <%= creator %> + <%= date %> + <%= annotation %> + + <%= yield %> + + \ No newline at end of file diff --git a/lib/templates/track.xml.erb b/lib/templates/track.xml.erb new file mode 100644 index 0000000..4c113fc --- /dev/null +++ b/lib/templates/track.xml.erb @@ -0,0 +1,3 @@ + +<% each_tag do |name,tag| %><%= "<#{name}>#{tag}\n" %> +<% end %> \ No newline at end of file From 8c6ebba2ef05d11c5f9e5334ea67da5438059475 Mon Sep 17 00:00:00 2001 From: iainb Date: Sun, 11 Mar 2018 14:50:27 +0000 Subject: [PATCH 07/34] Had Timecop in the gemspec. I don't like dev dependencies in the gemspec. --- Gemfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Gemfile b/Gemfile index e900703..5a0e828 100644 --- a/Gemfile +++ b/Gemfile @@ -21,4 +21,5 @@ group :test do gem "simplecov" gem "rspec" gem "rspec-given" + gem "timecop", ">=0.9.1" end From fb619a29e1430e48cdaac779f9f713e2faf05bc5 Mon Sep 17 00:00:00 2001 From: iainb Date: Sun, 11 Mar 2018 14:51:15 +0000 Subject: [PATCH 08/34] Added the dependency on Tilt and made it look pretty. --- spfy.gemspec | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spfy.gemspec b/spfy.gemspec index dddc4d2..5f08809 100644 --- a/spfy.gemspec +++ b/spfy.gemspec @@ -13,7 +13,8 @@ Gem::Specification.new do |gem| gem.bindir = "exe" gem.executables = gem.files.grep(%r{^exe/}).map{ |f| File.basename(f) } gem.add_runtime_dependency 'taglib-ruby', '>= 0.5.0' - gem.add_runtime_dependency "docopt", ">= 0.6.1" + gem.add_runtime_dependency "docopt", ">= 0.6.1" + gem.add_runtime_dependency "tilt", ">=2.0.8" gem.homepage = 'https://github.com/marcransome/Spfy' gem.license = 'GPL-3' end From f532c60b088a0f3c467012d18d7e2a281966ab6b Mon Sep 17 00:00:00 2001 From: iainb Date: Sun, 11 Mar 2018 16:06:10 +0000 Subject: [PATCH 09/34] Pkgsrc supports taglib. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 48e4b65..7847b97 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ A working Ruby installation (version 1.9 or greater) is required for Spfy to wor | Fedora/RHEL | `sudo yum install taglib-devel` | | Brew | `brew install taglib` | | MacPorts | `sudo port install taglib` | +| Pkgsrc | `(sudo) pkgin install taglib` | ### Installation With the prerequisites above taken care of Spfy can be installed with the following command: From 77bb9ab2550cec6977c1f55ae2162e8f71f90e7e Mon Sep 17 00:00:00 2001 From: iainb Date: Sun, 11 Mar 2018 16:27:33 +0000 Subject: [PATCH 10/34] Location should've been a URI, fixed. --- lib/spfy.rb | 14 +++++++++++++- spec/spfy_spec.rb | 16 +++++++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/lib/spfy.rb b/lib/spfy.rb index d4bfee1..dd8879f 100755 --- a/lib/spfy.rb +++ b/lib/spfy.rb @@ -2,6 +2,7 @@ require 'tilt' require 'pathname' require 'time' +require 'uri' # Create XSPF playlist files module Spfy @@ -22,7 +23,18 @@ def initialize path, options: {} # The file path attr_reader :path - alias_method :location, :path + + + # @see http://xspf.org/xspf-v1.html#rfc.section.4.1.1.2.5 + def location + if @location.nil? + path = @path.absolute? ? @path : @path.realpath + @location = URI.join('file:///', URI.escape(path.to_path) ) + end + @location +# rescue Errno::ENOENT => e + # Log somewhere + end # The targeted sub-elements attr_accessor :album, :artist, :comment, :genre, :title, :trackNum, :year diff --git a/spec/spfy_spec.rb b/spec/spfy_spec.rb index 7cda05a..bc2b2bd 100644 --- a/spec/spfy_spec.rb +++ b/spec/spfy_spec.rb @@ -17,7 +17,12 @@ def cmd str Given(:path) { Albums.join("mp4/mp4.m4a") } Given(:track) { Spfy::Track.new path } # Mung the path in the file so it works on any machine - Given(:xml_comparator) { Fixtures.join("mp4.m4a.xml").read.sub /FIXTURE/, Albums.join("mp4/mp4.m4a").to_path } + Given(:xml_comparator) { + track = Albums.join("mp4/mp4.m4a").to_path + Fixtures.join("mp4.m4a.xml") + .read + .sub /FIXTURE/, URI.join( "file:///", URI.escape( track )).to_s + } When(:xml) { track.to_xml } Then { cmd(xml.chomp) == cmd( xml_comparator.chomp ) } end @@ -45,8 +50,9 @@ def cmd str } Given(:playlist) { Spfy::Playlist.new( options ) } Given(:xml_comparator) { + track = Albums.join("mp4/mp4.m4a").to_path Fixtures.join("mp4.m4a.playlist.xml").read - .sub( /FIXTURE/, Albums.join("mp4/mp4.m4a").to_path ) + .sub( /FIXTURE/, URI.join( "file:///", URI.escape( track )).to_s ) .sub( /USER/, ENV["USER"] ) } When(:xml) { playlist.to_xml } @@ -80,7 +86,7 @@ def cmd str Given(:playlist) { Spfy::Playlist.new( options ) } Given(:xml_comparator) { Fixtures.join("mp3.playlist.xml").read - .gsub( /ALBUMS/, Albums.to_path ) + .gsub( /ALBUMS/, URI.join( "file:///", URI.escape( Albums.to_path)).to_s ) .sub( /USER/, ENV["USER"] ) } When(:xml) { playlist.to_xml } @@ -109,7 +115,7 @@ def cmd str Given(:playlist) { Spfy::Playlist.new( options ) } Given(:xml_comparator) { Fixtures.join("all-albums.playlist.xml").read - .gsub( /ALBUMS/, Albums.to_path ) + .gsub( /ALBUMS/, URI.join( "file:///", URI.escape( Albums.to_path)).to_s ) .sub( /USER/, ENV["USER"] ) } When(:xml) { playlist.to_xml } @@ -138,7 +144,7 @@ def cmd str Given(:playlist) { Spfy::Playlist.new( options ) } Given(:xml_comparator) { Fixtures.join("mp3-and-mp4.playlist.xml").read - .gsub( /ALBUMS/, Albums.to_path ) + .gsub( /ALBUMS/, URI.join( "file:///", URI.escape( Albums.to_path)).to_s ) .sub( /USER/, ENV["USER"] ) } When(:xml) { playlist.to_xml } From 4fe6b3f0424022e32d446f70abeb093c833cf0ed Mon Sep 17 00:00:00 2001 From: iainb Date: Mon, 12 Mar 2018 12:12:40 +0000 Subject: [PATCH 11/34] You are free to format RSpec as you wish. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7643d17..1beee35 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ backup/ scratch/ .yardoc/ spec/support/fixtures/albums +.rspec From 06b39a834ddb9358febc9dbd5288be5e1bf4035d Mon Sep 17 00:00:00 2001 From: iainb Date: Mon, 12 Mar 2018 12:16:20 +0000 Subject: [PATCH 12/34] Clean up the opts a bit; fixed potential bug. --- exe/spfy | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/exe/spfy b/exe/spfy index b4c5996..08c4da7 100755 --- a/exe/spfy +++ b/exe/spfy @@ -22,7 +22,7 @@ Options: --no-title Suppress track title in output --no-artist Suppress artist name in output --no-album Suppress album name in output - --no-tracknum Suppress track number in output + --no-trackNum Suppress track number in output - CASE SENSITIVE! --max-tracks NUM Limit the output to NUM tracks DOCOPT @@ -32,12 +32,18 @@ rescue Docopt::Exit => e puts e.message end +output = options.delete "--output" + +# Remove extraneous +options.delete "--help" +options.delete "--version" + playlist = Spfy::Playlist.new options blk = ->(f){ f.puts playlist.to_xml } if options["--output"] - File.open(options["--output"], "w", &blk) + File.open(output, "w", &blk) else blk.call(Kernel) end \ No newline at end of file From c1dc89090b40d44d224d5201e4716f8e947f8903 Mon Sep 17 00:00:00 2001 From: iainb Date: Mon, 12 Mar 2018 12:16:48 +0000 Subject: [PATCH 13/34] The --no- options work again. --- lib/spfy.rb | 134 ++++++++++++++++++++++++++++++--------------- spec/spfy_spec.rb | 137 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 224 insertions(+), 47 deletions(-) diff --git a/lib/spfy.rb b/lib/spfy.rb index dd8879f..71781a9 100755 --- a/lib/spfy.rb +++ b/lib/spfy.rb @@ -10,35 +10,107 @@ module Spfy # Locations of the ERB templates TEMPLATES = Pathname(__dir__).join("templates") + # Produces the track entity # @see http://xspf.org/xspf-v1.html#rfc.section.4.1.1.2.14.1.1 class Track + + XSPF_TAGS = [:location, :album, :artist, :comment, :genre, :title, :trackNum, :year].freeze + XSPF_TO_TAGLIB = { + :album => :album, + :artist => :artist, + :comment => :comment, + :genre => :genre, + :title => :title, + :trackNum => :track, + :year => :year, + }.freeze + + + # param [String,Pathname] path The location of the track. + # param [Hash] options def initialize path, options: {} @path = Pathname(path).expand_path @template = Tilt::ERBTemplate.new(TEMPLATES.join("track.xml.erb")) @processed = false - @available_tags = [:location, :album, :artist, :comment, :genre, :title, :trackNum, :year] + @options = options.dup + parse @options + @available_tags = XSPF_TAGS - @noes + @data_klass = Struct.new *@available_tags + @data = @data_klass.new + # @see http://xspf.org/xspf-v1.html#rfc.section.4.1.1.2.5 + if @location.nil? and @available_tags.include?(:location) + path = @path.absolute? ? @path : @path.realpath + @location = URI.join('file:///', URI.escape(path.to_path) ) + @data.location = @location + end + process end + + # If there's a way to do dynamic delegation using Forwadable + # I don't know what it is. Hence this. + def method_missing(name, *args) + if @available_tags.include? name.to_sym + instance_eval <<-RUBY + def #{name} + @data.send :#{name} + end + RUBY + send name.to_sym + else + super + end + end + + + # Be a good person + def respond_to?(name, include_private = false) + if @available_tags.include? name.to_sym + true + else + super + end + end + + + # Parse the options, mainly to find which ones were --no- + def parse options + @noes = options.each_with_object([]){|(k,v), obj| + if k =~ /^\-\-no\-/ and v + obj << k.match(/^\-\-no\-(?\w+)$/)[:name].to_sym + end + obj + } + end + # The file path attr_reader :path + attr_reader :available_tags + attr_reader :data - # @see http://xspf.org/xspf-v1.html#rfc.section.4.1.1.2.5 - def location - if @location.nil? - path = @path.absolute? ? @path : @path.realpath - @location = URI.join('file:///', URI.escape(path.to_path) ) + # @private + # Process the options into a track entity. + # Calls TagLib + def process refresh=false + if refresh or @processed == false + TagLib::FileRef.open(@path.to_path) do |fileref| + tags = fileref.tag + + unless tags.nil? # skip files with no tags + XSPF_TO_TAGLIB.select{|k,v| available_tags.include? k } + .each do |xspf,tagl| + @data.send "#{xspf}=", tags.send(tagl) + end + end + end + + @processed = true end - @location -# rescue Errno::ENOENT => e - # Log somewhere end - # The targeted sub-elements - attr_accessor :album, :artist, :comment, :genre, :title, :trackNum, :year - # For a block {|name,tag| ... } # @yield [name,tag] Yield the element name and its contents. @@ -57,29 +129,6 @@ def each_tag skip_nils=true end - # @private - # Process the options into a track entity. - # Calls TagLib - def process refresh=false - if refresh or @processed == false - TagLib::FileRef.open(@path.to_path) do |fileref| - tags = fileref.tag - - next if tags.nil? # skip files with no tags - - @album = tags.album - @artist = tags.artist - @comment = tags.comment - @genre = tags.genre - @title = tags.title - @trackNum = tags.track - @year = tags.year - end - @processed = true - end - end - - # The renderer # @return [String] def to_xml @@ -95,7 +144,7 @@ class Playlist def initialize options set_traps_for_signals - @options = options + @options = options.dup @files = [] @template = Tilt::ERBTemplate.new(TEMPLATES.join("playlist.xml.erb")) parse @options @@ -135,10 +184,10 @@ def date # "--version" => false # } def parse options - @paths = (options.fetch "PATHS", []).map{|path| Pathname(path.sub /^~/, ENV["HOME"]) } + @paths = (options.delete("PATHS") || []).map{|path| Pathname(path.sub /^~/, ENV["HOME"]) } return if @paths.empty? @title = if options["--title"] - options["--title"] + options.delete("--title") else if @paths.first.directory? @paths.first.basename @@ -146,9 +195,8 @@ def parse options @paths.first.parent.basename end end - @creator = options["--creator"] || ENV["USER"] - @annotation = options["--annotation"] || "Created with Spfy.rb" - @noes = options.select{|k,v| k =~ /^\-\-no\-/ and v } + @creator = options.delete("--creator") || ENV["USER"] + @annotation = options.delete("--annotation") || "Created with Spfy.rb" #@max_tracks = @option["--max-tracks"] end @@ -178,11 +226,11 @@ def to_xml Find.prune : next else - mapped << Spfy::Track.new(pn) + mapped << Spfy::Track.new(pn, options: @options) end end else - mapped << Spfy::Track.new(path) + mapped << Spfy::Track.new(path, options: @options) end } mapped.map(&:to_xml).join("\n") diff --git a/spec/spfy_spec.rb b/spec/spfy_spec.rb index bc2b2bd..935ac19 100644 --- a/spec/spfy_spec.rb +++ b/spec/spfy_spec.rb @@ -13,18 +13,147 @@ def cmd str end describe "Track" do + + context "Given some noes" do + Given(:path) { Albums.join("mp4/mp4.m4a") } + context "No location" do + Given(:options) { + { + "--no-location" => true, + "--no-title" => false, + "--no-artist" => false, + "--no-album" => false, + "--no-tracknum" => false, + } + } + Given(:track) { Spfy::Track.new path, options: options } + context "the unit" do + Then { track.instance_variable_get(:@noes) == [:location] } + + Then { track.available_tags.sort == (Spfy::Track::XSPF_TAGS - [:location]).sort } + And { track.respond_to?(:location) == false } + And { track.respond_to? :album } + And { track.respond_to? :artist } + And { track.respond_to? :comment } + And { track.respond_to? :genre } + And { track.respond_to? :title } + And { track.respond_to? :trackNum } + And { track.respond_to? :year } + And { track.respond_to? :data } + + Then { track.data.respond_to?(:location) == false } + And { track.data.respond_to? :album } + And { track.data.respond_to? :artist } + And { track.data.respond_to? :comment } + And { track.data.respond_to? :genre } + And { track.data.respond_to? :title } + And { track.data.respond_to? :trackNum } + And { track.data.respond_to? :year } + + Then { track.album == "Album" } + And { track.artist == "Artist" } + And { track.comment == "Comment" } + And { track.genre == "Pop" } + And { track.title == "Title" } + And { track.trackNum == 7 } + And { track.year == 2011 } + end + end + context "No year or genre" do + Given(:options) { + { + "--no-location" => false, + "--no-title" => false, + "--no-artist" => false, + "--no-album" => false, + "--no-tracknum" => false, + "--no-year" => true, + "--no-genre" => true, + } + } + Given(:track) { Spfy::Track.new path, options: options } + context "the unit" do + Then { track.instance_variable_get(:@noes).sort == [:genre,:year].sort } + + Then { track.available_tags.sort == (Spfy::Track::XSPF_TAGS - [:genre,:year]).sort } + And { track.respond_to?(:location) } + And { track.respond_to? :album } + And { track.respond_to? :artist } + And { track.respond_to? :comment } + And { track.respond_to?( :genre ) == false } + And { track.respond_to? :title } + And { track.respond_to? :trackNum } + And { track.respond_to?( :year ) == false} + And { track.respond_to? :data } + + Then { track.data.respond_to? :location } + And { track.data.respond_to? :album } + And { track.data.respond_to? :artist } + And { track.data.respond_to? :comment } + And { track.data.respond_to?( :genre ) == false } + And { track.data.respond_to? :title } + And { track.data.respond_to? :trackNum } + And { track.data.respond_to?( :year ) == false} + + Then { track.album == "Album" } + And { track.artist == "Artist" } + And { track.comment == "Comment" } + And { track.title == "Title" } + And { track.trackNum == 7 } + end + end + + end + context "Most simple case" do Given(:path) { Albums.join("mp4/mp4.m4a") } Given(:track) { Spfy::Track.new path } # Mung the path in the file so it works on any machine - Given(:xml_comparator) { + Given(:location) { track = Albums.join("mp4/mp4.m4a").to_path + URI.join( "file:///", URI.escape( track )) + } + Given(:xml_comparator) { Fixtures.join("mp4.m4a.xml") .read - .sub /FIXTURE/, URI.join( "file:///", URI.escape( track )).to_s + .sub /FIXTURE/, location.to_s } - When(:xml) { track.to_xml } - Then { cmd(xml.chomp) == cmd( xml_comparator.chomp ) } + context "the unit" do + Then { track.available_tags == Spfy::Track::XSPF_TAGS } + And { track.respond_to? :location } + And { track.respond_to? :album } + And { track.respond_to? :artist } + And { track.respond_to? :comment } + And { track.respond_to? :genre } + And { track.respond_to? :title } + And { track.respond_to? :trackNum } + And { track.respond_to? :year } + And { track.respond_to? :data } + + Then { track.data.respond_to? :location } + And { track.data.respond_to? :album } + And { track.data.respond_to? :artist } + And { track.data.respond_to? :comment } + And { track.data.respond_to? :genre } + And { track.data.respond_to? :title } + And { track.data.respond_to? :trackNum } + And { track.data.respond_to? :year } + + Then { track.location == location } + And { track.album == "Album" } + And { track.artist == "Artist" } + And { track.comment == "Comment" } + And { track.genre == "Pop" } + And { track.title == "Title" } + And { track.trackNum == 7 } + And { track.year == 2011 } + end + + + context "render" do + When(:xml) { track.to_xml } + Then { cmd(xml.chomp) == cmd( xml_comparator.chomp ) } + end end end From c1c51fbf308dfedff937c0c25a5a6c6730797c82 Mon Sep 17 00:00:00 2001 From: iainb Date: Mon, 12 Mar 2018 15:18:28 +0000 Subject: [PATCH 14/34] Max tracks option working. --- lib/spfy.rb | 17 ++++++----- spec/spfy_spec.rb | 73 +++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 71 insertions(+), 19 deletions(-) diff --git a/lib/spfy.rb b/lib/spfy.rb index 71781a9..e24938c 100755 --- a/lib/spfy.rb +++ b/lib/spfy.rb @@ -197,7 +197,7 @@ def parse options end @creator = options.delete("--creator") || ENV["USER"] @annotation = options.delete("--annotation") || "Created with Spfy.rb" - #@max_tracks = @option["--max-tracks"] + @max_tracks = options["--max-tracks"] && options["--max-tracks"].to_i end @@ -215,10 +215,9 @@ def set_traps_for_signals # Render the playlist and any tracks. def to_xml return "" if @paths.empty? - #catch :MaxTracksReached { - @template.render(self) do - mapped = [] - @paths.each { |path| + mapped = [] + catch(:MaxTracksReached){ + @paths.each do |path| if path.directory? path.find do |pn| if pn.directory? @@ -227,15 +226,19 @@ def to_xml next else mapped << Spfy::Track.new(pn, options: @options) + throw :MaxTracksReached if @max_tracks and mapped.size >= @max_tracks end end else mapped << Spfy::Track.new(path, options: @options) + throw :MaxTracksReached if @max_tracks and mapped.size >= @max_tracks end - } + end + } + # TODO fail when there are no tracks found + @template.render(self) do mapped.map(&:to_xml).join("\n") end - #} end end diff --git a/spec/spfy_spec.rb b/spec/spfy_spec.rb index 935ac19..aadf30d 100644 --- a/spec/spfy_spec.rb +++ b/spec/spfy_spec.rb @@ -161,8 +161,6 @@ def cmd str context "Most simple case", :time_sensitive do Given(:options) { { - "--help" => false, - "--output" => nil, "--title" => nil, "--creator" => nil, "--date" => nil, @@ -174,7 +172,6 @@ def cmd str "--no-tracknum" => false, "--max-tracks" => nil, "PATHS"=>["spec/support/fixtures/albums/mp4/mp4.m4a"], - "--version" => false } } Given(:playlist) { Spfy::Playlist.new( options ) } @@ -196,8 +193,6 @@ def cmd str context "A directory of files", :time_sensitive do Given(:options) { { - "--help" => false, - "--output" => nil, "--title" => nil, "--creator" => nil, "--date" => nil, @@ -209,7 +204,6 @@ def cmd str "--no-tracknum" => false, "--max-tracks" => nil, "PATHS"=>["spec/support/fixtures/albums/mp3"], - "--version" => false } } Given(:playlist) { Spfy::Playlist.new( options ) } @@ -225,8 +219,6 @@ def cmd str context "A directory of directories of files", :time_sensitive do Given(:options) { { - "--help" => false, - "--output" => nil, "--title" => nil, "--creator" => nil, "--date" => nil, @@ -238,7 +230,6 @@ def cmd str "--no-tracknum" => false, "--max-tracks" => nil, "PATHS"=>["spec/support/fixtures/albums"], - "--version" => false } } Given(:playlist) { Spfy::Playlist.new( options ) } @@ -254,8 +245,6 @@ def cmd str context "Multiple locations", :time_sensitive do Given(:options) { { - "--help" => false, - "--output" => nil, "--title" => nil, "--creator" => nil, "--date" => nil, @@ -267,7 +256,6 @@ def cmd str "--no-tracknum" => false, "--max-tracks" => nil, "PATHS"=>["spec/support/fixtures/albums/mp3", "spec/support/fixtures/albums/mp4"], - "--version" => false } } Given(:playlist) { Spfy::Playlist.new( options ) } @@ -279,4 +267,65 @@ def cmd str When(:xml) { playlist.to_xml } Then { cmd(xml.chomp) == cmd(xml_comparator.chomp) } end + + context "Given a max number of tracks" do + context "When the no. of tracks found is greater" do + Given(:options) { + { + "--title" => nil, + "--creator" => nil, + "--date" => nil, + "--annotation" => nil, + "--no-location" => false, + "--no-title" => false, + "--no-artist" => false, + "--no-album" => false, + "--no-tracknum" => false, + "--max-tracks" => 10, + "PATHS"=>["spec/support/fixtures/albums"], + } + } + Given(:playlist) { Spfy::Playlist.new( options ) } + When(:xml) { playlist.to_xml } + Then { + options["--max-tracks"].to_i == + cmd(xml.chomp).split("\n") + .select{|line| + line =~ /\/ + }.size + } + end + context "When the no. of tracks found is less", :time_sensitive do + Given(:options) { + { + "--title" => nil, + "--creator" => nil, + "--date" => nil, + "--annotation" => nil, + "--no-location" => false, + "--no-title" => false, + "--no-artist" => false, + "--no-album" => false, + "--no-tracknum" => false, + "--max-tracks" => 10, + "PATHS"=>["spec/support/fixtures/albums/mp3"], + } + } + Given(:playlist) { Spfy::Playlist.new( options ) } + Given(:xml_comparator) { + Fixtures.join("mp3.playlist.xml").read + .gsub( /ALBUMS/, URI.join( "file:///", URI.escape( Albums.to_path)).to_s ) + .sub( /USER/, ENV["USER"] ) + } + When(:xml) { playlist.to_xml } + Then { cmd(xml.chomp) == cmd(xml_comparator.chomp) } + Then { + options["--max-tracks"].to_i > + cmd(xml.chomp).split("\n") + .select{|line| + line =~ /\/ + }.size + } + end + end end \ No newline at end of file From 83d814bd6408714a4748ad751d1cfc3ef047d8b8 Mon Sep 17 00:00:00 2001 From: iainb Date: Mon, 12 Mar 2018 15:31:01 +0000 Subject: [PATCH 15/34] Didn't replace the option with the variable and it caused a bug, fixed. --- exe/spfy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exe/spfy b/exe/spfy index 08c4da7..1fb14f4 100755 --- a/exe/spfy +++ b/exe/spfy @@ -42,7 +42,7 @@ playlist = Spfy::Playlist.new options blk = ->(f){ f.puts playlist.to_xml } -if options["--output"] +if output File.open(output, "w", &blk) else blk.call(Kernel) From 2260697a1fff1993c82a6a64949f2828f949607b Mon Sep 17 00:00:00 2001 From: iainb Date: Mon, 12 Mar 2018 15:32:54 +0000 Subject: [PATCH 16/34] Needed the exit or errors on calling help. --- exe/spfy | 1 + 1 file changed, 1 insertion(+) diff --git a/exe/spfy b/exe/spfy index 1fb14f4..8163c4a 100755 --- a/exe/spfy +++ b/exe/spfy @@ -30,6 +30,7 @@ begin options = Docopt::docopt(doc, version: Spfy::VERSION) rescue Docopt::Exit => e puts e.message + exit 0 end output = options.delete "--output" From 36336526aa7bfae48b6fd5f6507089f2bacca602 Mon Sep 17 00:00:00 2001 From: iainb Date: Tue, 13 Mar 2018 11:05:23 +0000 Subject: [PATCH 17/34] Added a --use-tidy and a --force option. The --use-tidy option is to prettify the output. I can't get ERB to do this, I tried Haml but that just introduced other problems. I decided to leave it up to `tidy` as it works well in the specs. The --force option is to stop files being destroyed/overwritten, whether maliciously or by accident. --- exe/spfy | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/exe/spfy b/exe/spfy index 8163c4a..625ba4b 100755 --- a/exe/spfy +++ b/exe/spfy @@ -24,6 +24,10 @@ Options: --no-album Suppress album name in output --no-trackNum Suppress track number in output - CASE SENSITIVE! --max-tracks NUM Limit the output to NUM tracks + --use-tidy Run the tidy command to prettify the output. + Uses `/usr/bin/command -v tidy` to find tidy and + `tidy -q -i -xml` to filter through. + --force Allow the overwriting of a file. DOCOPT begin @@ -38,13 +42,41 @@ output = options.delete "--output" # Remove extraneous options.delete "--help" options.delete "--version" +use_tidy = options.delete "--use-tidy" +force_ok = options.delete "--force" +WARNING_RED="tput setaf 1" +BACK_TO_NORMAL="tput sgr0" playlist = Spfy::Playlist.new options -blk = ->(f){ f.puts playlist.to_xml } +COMMAND = "/usr/bin/command" +DEFAULT_TIDY_FINDER="#{COMMAND} -v tidy" + +def filter xml, use_tidy=false + if use_tidy + require 'open3' + path = Pathname(`#{DEFAULT_TIDY_FINDER}`.chomp) + if path.exist? + stdout, _= Open3.capture2(path.to_path,"-q", "-i", "-xml", stdin_data: xml) + stdout + else + warn `#{WARNING_RED}` + "tidy not found at #{path.to_path}" + `#{BACK_TO_NORMAL}` + "\nRunning without tidying…\n\n" + xml + end + else + xml + end +end + +blk = ->(f){ f.puts filter(playlist.to_xml, use_tidy) } if output - File.open(output, "w", &blk) + path = Pathname(output) + if path.exist? and not force_ok + warn `#{WARNING_RED}` + "#{path.to_path} exists. Use the --force option to overwrite" + `#{BACK_TO_NORMAL}` + else + File.open(path, "w", &blk) + end else blk.call(Kernel) end \ No newline at end of file From 8d50c10b2ab16a959c6576916ef479e8345a780b Mon Sep 17 00:00:00 2001 From: iainb Date: Tue, 13 Mar 2018 11:18:40 +0000 Subject: [PATCH 18/34] Updated the instructions. --- README.md | 56 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 7847b97..062564e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ **Spfy** is a command-line tool for generating [XSPF](http://xspf.org/) playlists from metadata stored in several popular audio formats and is developed entirely in [Ruby](http://www.ruby-lang.org/). It takes one or more local directory paths as input, extracts metadata tags from any audio files that it encounters, and generates a valid XSPF playlist. ### Prerequisites -A working Ruby installation (version 1.9 or greater) is required for Spfy to work, but this is outside the scope of this guide. For more information refer to the [official installation procedure](http://www.ruby-lang.org/en/downloads/). +A working Ruby installation (version 2.0 or greater) is required for Spfy to work, but this is outside the scope of this guide. For more information refer to the [official installation procedure](http://www.ruby-lang.org/en/downloads/). [TagLib](http://developer.kde.org/~wheeler/taglib.html) is also required. Follow the steps below (taken from the [taglib-ruby installation guide](http://robinst.github.com/taglib-ruby/)) to install the necessary files for your respective system type: @@ -22,19 +22,23 @@ With the prerequisites above taken care of Spfy can be installed with the follow $ gem install spfy ### Using Spfy -By default, Spfy will output a formatted XSPF playlist to the standard output stream that will include _location_, _title_, _creator_, _album_, and _trackNum_ elements for each audio file where available. +By default, Spfy will output a formatted XSPF playlist to the standard output stream that will include _location_, _album_, _artist_, _comment_, _genre_, _title_, _trackNum_, and _year_ elements for each audio file where available. The general syntax for Spfy is `spfy [options] dir1 ... dirN`, where _dir1 ... dirN_ is one or more paths to directories containing audio files. For example: - $ spfy ~/music + $ spfy ~/music/"Smashing Pumpkins" -..will produce the following output (where ~/music contains one audio file with valid metadata): +..will produce the following output (where ~/music/Smashing\ Pumpkins contains one audio file with valid metadata): ```xml + Smashing Pumpkins + bobby (or whatever your username is) + 2018-03-11T06:49:16+00:00 + Created with Spfy.rb file:///Users/spfy/music/03%20A%20Stitch%20In%20Time.mp3 @@ -47,25 +51,55 @@ For example: ``` -Spfy supports multiple directory paths (e.g. `spfy /dir1 /dir2`) and traverses each directory recursively by default. Unsupported files and empty directories in a directory tree are silently ignored and will not impact Spfy's output. +Spfy supports multiple directory paths (e.g. `spfy /dir1 /dir2`) and traverses each directory recursively by default. Unsupported files and empty directories in a directory tree are silently ignored and will not impact Spfy's output. It will even take a single file as a target, or directories with sub-directories that hold files (no max depth has been set on the recursion so don't start to far up the directory tree;) Command-line arguments allow you to control which elements Spfy outputs: - -f, --no-location Suppress file location output - -t, --no-title Suppress track title in output - -a, --no-artist Suppress artist name in output - -l, --no-album Suppress album name in output - -n, --no-tracknum Suppress track number in output + --no-location Suppress file location output + --no-title Suppress track title in output + --no-artist Suppress artist name in output + --no-album Suppress album name in output + --no-trackNum Suppress track number in output - CASE SENSITIVE! + +You can also control the metadata for the playlist: + + -t TITLE --title=TITLE Playlist title + -c CREATOR --creator=CREATOR Playlist creator, defaults to env $USER + -d DATE --date=DATE Playlist creation date, defaults to now + -a NOTE --annotation=NOTE Playlist annotation, default: "Created with Spfy.rb" + +Specify an output file: + + -o FILE --output=FILE File to write to, otherwise output to STDOUT + --force Allow the overwriting of a file. + +Limit the number of tracks: + + --max-tracks NUM Limit the output to NUM tracks + +And prettify the output: + + --use-tidy Run the tidy command to prettify the output. + Uses `/usr/bin/command -v tidy` to find tidy and + `tidy -q -i -xml` to filter through. + +Although you could simply not specify an outfile and pipe through a filter of your choice to get the same effect. For additional options use `spfy --help`. ### License Spfy is free software, and you are welcome to redistribute it under certain conditions. See the [GNU General Public License](http://www.gnu.org/licenses/gpl.html) for more details. + +### Development + +Use Bundler to install the development dependencies and then run the Rake task to copy the audio files for the specs to run against. + + ### Acknowledgements Spfy uses the following third party software components: * [taglib-ruby](http://robinst.github.com/taglib-ruby/) by Robin Stocker ### Contact -Email me at [marc.ransome@fidgetbox.co.uk](mailto:marc.ransome@fidgetbox.co.uk) or tweet [@marcransome](http://www.twitter.com/marcransome). +Email me at [marc.ransome@fidgetbox.co.uk](mailto_marc_.ransome@fidgetbox.co.uk) or tweet [@marcransome](http://www.twitter.com/marcransome). From 99e3673f10a0141bd5d4a0a1fddd0213f7d9c9c5 Mon Sep 17 00:00:00 2001 From: iainb Date: Tue, 13 Mar 2018 11:45:41 +0000 Subject: [PATCH 19/34] gem build complains about this line, fixed. --- spfy.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spfy.gemspec b/spfy.gemspec index 5f08809..ef9a747 100644 --- a/spfy.gemspec +++ b/spfy.gemspec @@ -16,5 +16,5 @@ Gem::Specification.new do |gem| gem.add_runtime_dependency "docopt", ">= 0.6.1" gem.add_runtime_dependency "tilt", ">=2.0.8" gem.homepage = 'https://github.com/marcransome/Spfy' - gem.license = 'GPL-3' + gem.license = 'GPL-3.0' end From 6d021d74768cb9b33c1f25046856bf2f318c293e Mon Sep 17 00:00:00 2001 From: iainb Date: Tue, 13 Mar 2018 11:46:23 +0000 Subject: [PATCH 20/34] Version wasn't picked up, fixed; full path looks messy, hence Pathname. --- exe/spfy | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/exe/spfy b/exe/spfy index 625ba4b..e2e10d2 100755 --- a/exe/spfy +++ b/exe/spfy @@ -1,14 +1,18 @@ #!/usr/bin/env ruby require 'spfy' +require 'spfy/version' require 'docopt' +require 'pathname' + +EXE = Pathname(__FILE__).basename doc = < Date: Tue, 13 Mar 2018 14:44:12 +0000 Subject: [PATCH 21/34] No longer using this parser. --- lib/spfy/optionreader.rb | 93 ---------------------------------------- 1 file changed, 93 deletions(-) delete mode 100644 lib/spfy/optionreader.rb diff --git a/lib/spfy/optionreader.rb b/lib/spfy/optionreader.rb deleted file mode 100644 index 6009254..0000000 --- a/lib/spfy/optionreader.rb +++ /dev/null @@ -1,93 +0,0 @@ -module Spfy - class OptionReader - - def self.parse(args) - - options = OpenStruct.new - options.output = [] - options.verbose = false - options.hide_title = false - options.hide_artist = false - options.hide_album = false - options.hide_location = false - options.hide_tracknum = false - options.dirs = [] - options.tracks_to_process = [] - - opts = OptionParser.new do |opts| - opts.banner = "Usage: #{File.basename($0)} [options] dir1 ... dirN" - - opts.separator "" - - opts.separator "#{File.basename($0).capitalize} supports multiple directory paths and traverses each directory recursively." - opts.separator "Non-audio files are ignored and will not impact #{File.basename($0)}'s output." - - opts.separator "" - opts.separator "Output options:" - - opts.on("-o", "--output FILE", "File to output XSPF data to") do |out| - options.output << out - end - - opts.on("-f", "--no-location", "Suppress file location output") do - options.hide_location = true - end - - opts.on("-t", "--no-title", "Suppress track title in output") do - options.hide_title = true - end - - opts.on("-a", "--no-artist", "Suppress artist name in output") do - options.hide_artist = true - end - - opts.on("-l", "--no-album", "Suppress album name in output") do - options.hide_album = true - end - - opts.on("-n", "--no-tracknum", "Suppress track number in output") do - options.hide_tracknum = true - end - - opts.on("-m", "--max-tracks NUM", "Limit the output to NUM tracks") do |num| - options.tracks_to_process << num - options.track_limit = true - end - - opts.separator "" - opts.separator "Misc options:" - - opts.on("-v", "--version", "Display version information") do - puts "#{File.basename($0).capitalize} #{Spfy::VERSION} Copyright (c) 2012 Marc Ransome " - puts "This program comes with ABSOLUTELY NO WARRANTY, use it at your own risk." - puts "This is free software, and you are welcome to redistribute it under" - puts "certain conditions; see LICENSE.txt for details." - exit - end - - opts.on_tail("-h", "--help", "Show this screen") do - puts opts - exit - end - end - - # parse then remove the remaining arguments - opts.parse!(args) - - # test leftover input for valid paths - args.each do |dir| - - # add path to global dirs variable - if File.directory?(dir) - options.dirs << dir - end - - end - - # return the options array - options - - end # def self.parse(args) - - end # class OptionReader -end From 2228420f003a9117ac914f2b87c26bdd9f8fa114 Mon Sep 17 00:00:00 2001 From: iainb Date: Tue, 13 Mar 2018 14:44:37 +0000 Subject: [PATCH 22/34] Using yard for docs and yardstick to help improve the docs. --- Gemfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Gemfile b/Gemfile index 5a0e828..1358794 100644 --- a/Gemfile +++ b/Gemfile @@ -12,6 +12,8 @@ group :development do gem "pry-state" gem "rb-readline" gem "awesome_print" + gem "yard" + gem "yardstick" end From 4df5955b8a156947f82228122a81936c1fe4c162 Mon Sep 17 00:00:00 2001 From: iainb Date: Tue, 13 Mar 2018 14:45:56 +0000 Subject: [PATCH 23/34] Improved the yard docs based on yardstick's pedantry. --- lib/spfy.rb | 78 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 8 deletions(-) diff --git a/lib/spfy.rb b/lib/spfy.rb index e24938c..1269528 100755 --- a/lib/spfy.rb +++ b/lib/spfy.rb @@ -15,7 +15,10 @@ module Spfy # @see http://xspf.org/xspf-v1.html#rfc.section.4.1.1.2.14.1.1 class Track + # The XSPF tags being targeted XSPF_TAGS = [:location, :album, :artist, :comment, :genre, :title, :trackNum, :year].freeze + + # The translation of XSPF tags and Taglib tags XSPF_TO_TAGLIB = { :album => :album, :artist => :artist, @@ -27,8 +30,11 @@ class Track }.freeze - # param [String,Pathname] path The location of the track. - # param [Hash] options + # @param [String,Pathname] path The location of the track. + # @param [Hash] options + # @api public + # @example + # Spfy::Track.new(pn, options: @options) def initialize path, options: {} @path = Pathname(path).expand_path @template = Tilt::ERBTemplate.new(TEMPLATES.join("track.xml.erb")) @@ -51,6 +57,8 @@ def initialize path, options: {} # If there's a way to do dynamic delegation using Forwadable # I don't know what it is. Hence this. + # @api private + # @see https://ruby-doc.org/core-2.5.0/BasicObject.html#method-i-method_missing def method_missing(name, *args) if @available_tags.include? name.to_sym instance_eval <<-RUBY @@ -66,7 +74,10 @@ def #{name} # Be a good person - def respond_to?(name, include_private = false) + # @api public + # @return [TrueClass] + # @see https://ruby-doc.org/core-2.5.0/Object.html#method-i-respond_to_missing-3F + def respond_to_missing?(name, *) if @available_tags.include? name.to_sym true else @@ -76,6 +87,8 @@ def respond_to?(name, include_private = false) # Parse the options, mainly to find which ones were --no- + # @api private + # @return [] Doesn't matter, it's held in the @noes instance var. def parse options @noes = options.each_with_object([]){|(k,v), obj| if k =~ /^\-\-no\-/ and v @@ -85,15 +98,28 @@ def parse options } end + # The file path + # @return [Pathname] + # @api public attr_reader :path + + + # The available tags after the options have been parsed + # @return [] + # @api public attr_reader :available_tags + + + # A data object that holds the taglib data in XSPF format + # @return [Struct] + # @api semipublic attr_reader :data - # @private # Process the options into a track entity. # Calls TagLib + # @api private def process refresh=false if refresh or @processed == false TagLib::FileRef.open(@path.to_path) do |fileref| @@ -114,6 +140,10 @@ def process refresh=false # For a block {|name,tag| ... } # @yield [name,tag] Yield the element name and its contents. + # @api public + # @example + # # In ERB + # <% each_tag do |name,tag| %><%= "<#{name}>#{tag}\n" %> def each_tag skip_nils=true return enum_for(:each_tag) unless block_given? @available_tags.each do |tag| @@ -131,6 +161,7 @@ def each_tag skip_nils=true # The renderer # @return [String] + # @api public def to_xml process @template.render(self) @@ -140,8 +171,12 @@ def to_xml # Produces the playlist entity # @see http://xspf.org/xspf-v1.html#rfc.section.4.1.1 + # @api public class Playlist + # @api public + # @example + # playlist = Spfy::Playlist.new options def initialize options set_traps_for_signals @options = options.dup @@ -150,22 +185,41 @@ def initialize options parse @options end - attr_reader :creator, :title, :options, :paths + + # @see http://xspf.org/xspf-v1.html#rfc.section.4.1.1.2.2 + # @return [String] + attr_reader :creator + + # @see http://xspf.org/xspf-v1.html#rfc.section.4.1.1.2.2 + # @return [String] + attr_reader :title + + + # @return [Hash] The options hash passed in. + attr_reader :options + + # The paths that will be searched for files. + # @return [] + attr_reader :paths + # @see http://xspf.org/xspf-v1.html#rfc.section.4.1.1.2.3 + # @api public + # @return [String] def annotation @annotation end # @see http://xspf.org/xspf-v1.html#rfc.section.4.1.1.2.8 + # @api public + # @return [String] Time now formatted as an ISO8601 date. def date Time.now.iso8601 end - # @private - # The option parser + # Parse the options further to set needed instance variables. # spfy here there and everywhere # { # "--help" => false, @@ -183,6 +237,8 @@ def date # "PATHS"=>["spec/support/fixtures/albums/mp4/mp4.m4a"], # "--version" => false # } + # @api private + # @return [Hash] Ignore the return value, this is a side-effect, it sets several instance variables def parse options @paths = (options.delete("PATHS") || []).map{|path| Pathname(path.sub /^~/, ENV["HOME"]) } return if @paths.empty? @@ -198,11 +254,13 @@ def parse options @creator = options.delete("--creator") || ENV["USER"] @annotation = options.delete("--annotation") || "Created with Spfy.rb" @max_tracks = options["--max-tracks"] && options["--max-tracks"].to_i + options end - # @private # For interruptions + # @api private + # @return There is no return value, it's a side-effect def set_traps_for_signals trap(:SIGINT) do warn " Received Ctrl+c" @@ -213,6 +271,10 @@ def set_traps_for_signals # Render the playlist and any tracks. + # @api public + # @example + # playlist.to_xml + # @return [String] XML output def to_xml return "" if @paths.empty? mapped = [] From 2ed317a3f3e03c1dc9122c6786359940954cad76 Mon Sep 17 00:00:00 2001 From: iainb Date: Tue, 13 Mar 2018 14:51:49 +0000 Subject: [PATCH 24/34] Fixed a typo, kind of fixed formatting of the table for markdown. Kind of. --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 062564e..38b1f74 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,13 @@ A working Ruby installation (version 2.0 or greater) is required for Spfy to wor [TagLib](http://developer.kde.org/~wheeler/taglib.html) is also required. Follow the steps below (taken from the [taglib-ruby installation guide](http://robinst.github.com/taglib-ruby/)) to install the necessary files for your respective system type: -| System: | Command: | -|---------------|------------------------------------| -| Debian/Ubuntu | `sudo apt-get install libtag1-dev` | -| Fedora/RHEL | `sudo yum install taglib-devel` | -| Brew | `brew install taglib` | -| MacPorts | `sudo port install taglib` | -| Pkgsrc | `(sudo) pkgin install taglib` | +| System: | Command: | +|---------------|------------------------------------| +| Debian/Ubuntu | `sudo apt-get install libtag1-dev` | +| Fedora/RHEL | `sudo yum install taglib-devel` | +| Brew | `brew install taglib` | +| MacPorts | `sudo port install taglib` | +| Pkgsrc | `(sudo) pkgin install taglib` | ### Installation With the prerequisites above taken care of Spfy can be installed with the following command: @@ -51,7 +51,7 @@ For example: ``` -Spfy supports multiple directory paths (e.g. `spfy /dir1 /dir2`) and traverses each directory recursively by default. Unsupported files and empty directories in a directory tree are silently ignored and will not impact Spfy's output. It will even take a single file as a target, or directories with sub-directories that hold files (no max depth has been set on the recursion so don't start to far up the directory tree;) +Spfy supports multiple directory paths (e.g. `spfy /dir1 /dir2`) and traverses each directory recursively by default. Unsupported files and empty directories in a directory tree are silently ignored and will not impact Spfy's output. It will even take a single file as a target, or directories with sub-directories that hold files (no max depth has been set on the recursion so don't start too far up the directory tree;) Command-line arguments allow you to control which elements Spfy outputs: From ff76b44669e4e9803747270113cf9c5514c4abde Mon Sep 17 00:00:00 2001 From: iainb Date: Tue, 13 Mar 2018 14:55:48 +0000 Subject: [PATCH 25/34] Added CHANGELOG, in the style of Keep a Changelog. --- CHANGELOG.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a8a83a6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,31 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## Unreleased + +### Added + +- Start using [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). +- A separate version file, lib/spfy/version.rb +- Lots of Yard docs. +- Specs. +- Command line options to control the meta-data of a playlist. +- Option to prettify the output via `tidy` + +### Changed + +- Start following [SemVer](http://semver.org) properly. +- Updated gemspec to current standards (2018) e.g. a separate version file, git ls-files etc +- Moved executable from bin/ to exe/ so as not to get caught up with binstubs. +- Using docopt for parsing, it's so much easier to work with than other opt-parsers. +- Moved from building up the XML line by line, tag by tag, to running it through ERB templates. +- More object orientated design. +- Some of the options for suppressing output. + +## [1.0.0] + +- A working product at this point. \ No newline at end of file From ce08f2677f0aa14d98091b6edf3b53781f96f82f Mon Sep 17 00:00:00 2001 From: iainb Date: Wed, 14 Mar 2018 10:17:04 +0000 Subject: [PATCH 26/34] Output errors. --- exe/spfy | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/exe/spfy b/exe/spfy index e2e10d2..2ca4007 100755 --- a/exe/spfy +++ b/exe/spfy @@ -74,6 +74,7 @@ end blk = ->(f){ f.puts filter(playlist.to_xml, use_tidy) } +begin if output path = Pathname(output) if path.exist? and not force_ok @@ -83,4 +84,7 @@ if output end else blk.call(Kernel) +end +rescue => e + warn e.message end \ No newline at end of file From 541ef78d95e66dc230f63d003cf5a37353abbf63 Mon Sep 17 00:00:00 2001 From: iainb Date: Wed, 14 Mar 2018 10:25:44 +0000 Subject: [PATCH 27/34] Bail early with error if no tracks found; warn if path(s) not exist; tag all exceptions to be helpful. --- lib/spfy.rb | 20 ++++++++++++++++++-- spec/spfy_spec.rb | 20 ++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/lib/spfy.rb b/lib/spfy.rb index 1269528..5532695 100755 --- a/lib/spfy.rb +++ b/lib/spfy.rb @@ -7,6 +7,10 @@ # Create XSPF playlist files module Spfy + # For tagging all exceptions that come through this library. + module Error; end + + # Locations of the ERB templates TEMPLATES = Pathname(__dir__).join("templates") @@ -280,6 +284,10 @@ def to_xml mapped = [] catch(:MaxTracksReached){ @paths.each do |path| + if !path.exist? + warn "#{path.to_path} does not exist" + next + end if path.directory? path.find do |pn| if pn.directory? @@ -297,11 +305,19 @@ def to_xml end end } - # TODO fail when there are no tracks found + if mapped.size.zero? + fail RuntimeError, "No tracks found in #{@paths.map(&:to_path).join(' or ')}" + end @template.render(self) do mapped.map(&:to_xml).join("\n") end - end + rescue Spfy::Error + raise + rescue => error + # Tag any exceptions coming through this library + error.extend(Spfy::Error) + raise + end end end \ No newline at end of file diff --git a/spec/spfy_spec.rb b/spec/spfy_spec.rb index aadf30d..13ef44d 100644 --- a/spec/spfy_spec.rb +++ b/spec/spfy_spec.rb @@ -328,4 +328,24 @@ def cmd str } end end + + context "Given paths with no media files" do + Given(:options) { + { + "--title" => nil, + "--creator" => nil, + "--date" => nil, + "--annotation" => nil, + "--no-location" => false, + "--no-title" => false, + "--no-artist" => false, + "--no-album" => false, + "--no-tracknum" => false, + "--max-tracks" => 10, + "PATHS"=>["spec/support/fixtures/albums/empty"], + } + } + Given(:playlist) { Spfy::Playlist.new( options ) } + Then { expect { playlist.to_xml }.to raise_error(Spfy::Error) } + end end \ No newline at end of file From 5d649e6cac68e5d5a4ba6d5e98ab9392a8d83599 Mon Sep 17 00:00:00 2001 From: iainb Date: Wed, 14 Mar 2018 10:44:36 +0000 Subject: [PATCH 28/34] Forgot to rename var, and make the dir for the empty directory test. --- Rakefile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Rakefile b/Rakefile index ce8ec1a..dde6a5c 100644 --- a/Rakefile +++ b/Rakefile @@ -5,10 +5,10 @@ task :fixtures do gem_home = Pathname(ENV["GEM_HOME"]) files = gem_home.join("gems").glob("taglib-ruby-*/test/data/*").reject{|x| x.extname.to_s == ".cpp" } # mk dir tree - spec_dir = Pathname(__dir__).join("spec/support/fixtures/albums") - spec_dir.mkpath + albums_dir = Pathname(__dir__).join("spec/support/fixtures/albums") + albums_dir.mkpath %w{flac mp3 mp4 oga wav}.each do |ext| - album = spec_dir.join(ext) + album = albums_dir.join(ext) album.mkpath # find taglib and copy files # And yes, this is a horrible hack @@ -22,4 +22,5 @@ task :fixtures do end end puts "Audio files copied from taglib to spec/support/fixtures/albums" + albums_dir.join("empty").mkpath # for the empty directory test end \ No newline at end of file From cce5de6a67da564dce90957fa75dd4c7f6332c18 Mon Sep 17 00:00:00 2001 From: iainb Date: Wed, 14 Mar 2018 10:45:14 +0000 Subject: [PATCH 29/34] Ignore the generated gem files. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1beee35..4177d71 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ scratch/ .yardoc/ spec/support/fixtures/albums .rspec +*.gem From 6493c6c53bb57cc090b885fa7fd9a72390452a69 Mon Sep 17 00:00:00 2001 From: iainb Date: Wed, 14 Mar 2018 12:24:28 +0000 Subject: [PATCH 30/34] RSpec listed twice, fixed. --- Gemfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Gemfile b/Gemfile index 1358794..cbd1c52 100644 --- a/Gemfile +++ b/Gemfile @@ -21,7 +21,6 @@ group :test do gem "rspec" gem "rspec-its" gem "simplecov" - gem "rspec" gem "rspec-given" gem "timecop", ">=0.9.1" end From 2027a44c16752e9302a338e5fd1718ddb460a779 Mon Sep 17 00:00:00 2001 From: iainb Date: Wed, 14 Mar 2018 12:25:24 +0000 Subject: [PATCH 31/34] Use Addressable, it escapes paths better than stdlib URI. --- lib/spfy.rb | 4 ++-- spec/spfy_spec.rb | 12 ++++++------ spfy.gemspec | 1 + 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/spfy.rb b/lib/spfy.rb index 5532695..af8091c 100755 --- a/lib/spfy.rb +++ b/lib/spfy.rb @@ -2,7 +2,7 @@ require 'tilt' require 'pathname' require 'time' -require 'uri' +require 'addressable/uri' # Create XSPF playlist files module Spfy @@ -51,7 +51,7 @@ def initialize path, options: {} # @see http://xspf.org/xspf-v1.html#rfc.section.4.1.1.2.5 if @location.nil? and @available_tags.include?(:location) path = @path.absolute? ? @path : @path.realpath - @location = URI.join('file:///', URI.escape(path.to_path) ) + @location = Addressable::URI.join('file:///', Addressable::URI.escape(path.to_path) ) @data.location = @location end diff --git a/spec/spfy_spec.rb b/spec/spfy_spec.rb index 13ef44d..af3aa5d 100644 --- a/spec/spfy_spec.rb +++ b/spec/spfy_spec.rb @@ -111,7 +111,7 @@ def cmd str # Mung the path in the file so it works on any machine Given(:location) { track = Albums.join("mp4/mp4.m4a").to_path - URI.join( "file:///", URI.escape( track )) + Addressable::URI.join( "file:///", Addressable::URI.escape( track )) } Given(:xml_comparator) { Fixtures.join("mp4.m4a.xml") @@ -178,7 +178,7 @@ def cmd str Given(:xml_comparator) { track = Albums.join("mp4/mp4.m4a").to_path Fixtures.join("mp4.m4a.playlist.xml").read - .sub( /FIXTURE/, URI.join( "file:///", URI.escape( track )).to_s ) + .sub( /FIXTURE/, Addressable::URI.join( "file:///", Addressable::URI.escape( track )).to_s ) .sub( /USER/, ENV["USER"] ) } When(:xml) { playlist.to_xml } @@ -209,7 +209,7 @@ def cmd str Given(:playlist) { Spfy::Playlist.new( options ) } Given(:xml_comparator) { Fixtures.join("mp3.playlist.xml").read - .gsub( /ALBUMS/, URI.join( "file:///", URI.escape( Albums.to_path)).to_s ) + .gsub( /ALBUMS/, Addressable::URI.join( "file:///", Addressable::URI.escape( Albums.to_path)).to_s ) .sub( /USER/, ENV["USER"] ) } When(:xml) { playlist.to_xml } @@ -235,7 +235,7 @@ def cmd str Given(:playlist) { Spfy::Playlist.new( options ) } Given(:xml_comparator) { Fixtures.join("all-albums.playlist.xml").read - .gsub( /ALBUMS/, URI.join( "file:///", URI.escape( Albums.to_path)).to_s ) + .gsub( /ALBUMS/, Addressable::URI.join( "file:///", Addressable::URI.escape( Albums.to_path)).to_s ) .sub( /USER/, ENV["USER"] ) } When(:xml) { playlist.to_xml } @@ -261,7 +261,7 @@ def cmd str Given(:playlist) { Spfy::Playlist.new( options ) } Given(:xml_comparator) { Fixtures.join("mp3-and-mp4.playlist.xml").read - .gsub( /ALBUMS/, URI.join( "file:///", URI.escape( Albums.to_path)).to_s ) + .gsub( /ALBUMS/, Addressable::URI.join( "file:///", Addressable::URI.escape( Albums.to_path)).to_s ) .sub( /USER/, ENV["USER"] ) } When(:xml) { playlist.to_xml } @@ -314,7 +314,7 @@ def cmd str Given(:playlist) { Spfy::Playlist.new( options ) } Given(:xml_comparator) { Fixtures.join("mp3.playlist.xml").read - .gsub( /ALBUMS/, URI.join( "file:///", URI.escape( Albums.to_path)).to_s ) + .gsub( /ALBUMS/, Addressable::URI.join( "file:///", Addressable::URI.escape( Albums.to_path)).to_s ) .sub( /USER/, ENV["USER"] ) } When(:xml) { playlist.to_xml } diff --git a/spfy.gemspec b/spfy.gemspec index ef9a747..4ec2a19 100644 --- a/spfy.gemspec +++ b/spfy.gemspec @@ -15,6 +15,7 @@ Gem::Specification.new do |gem| gem.add_runtime_dependency 'taglib-ruby', '>= 0.5.0' gem.add_runtime_dependency "docopt", ">= 0.6.1" gem.add_runtime_dependency "tilt", ">=2.0.8" + gem.add_runtime_dependency "addressable", ">=2.5.2" gem.homepage = 'https://github.com/marcransome/Spfy' gem.license = 'GPL-3.0' end From 44bc9c5f398a74dacfe8f277e16216bdaff80862 Mon Sep 17 00:00:00 2001 From: iainb Date: Wed, 14 Mar 2018 16:41:45 +0000 Subject: [PATCH 32/34] Make sure extra whitespace isn't introduced, it has a habit of mucking up XML decoders. --- lib/spfy.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/spfy.rb b/lib/spfy.rb index af8091c..53e4048 100755 --- a/lib/spfy.rb +++ b/lib/spfy.rb @@ -14,7 +14,6 @@ module Error; end # Locations of the ERB templates TEMPLATES = Pathname(__dir__).join("templates") - # Produces the track entity # @see http://xspf.org/xspf-v1.html#rfc.section.4.1.1.2.14.1.1 class Track @@ -132,7 +131,7 @@ def process refresh=false unless tags.nil? # skip files with no tags XSPF_TO_TAGLIB.select{|k,v| available_tags.include? k } .each do |xspf,tagl| - @data.send "#{xspf}=", tags.send(tagl) + @data.send "#{xspf}=", tags.respond_to?(:strip) ? tags.send(tagl).strip : tags.send(tagl) end end end From e1a3ed422b3d1e651e96d7bf29c85e5ca89f78d3 Mon Sep 17 00:00:00 2001 From: iainb Date: Wed, 14 Mar 2018 16:42:28 +0000 Subject: [PATCH 33/34] Properly encode text in XML, decoders don't like stray ampersands and the like. --- lib/templates/track.xml.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/templates/track.xml.erb b/lib/templates/track.xml.erb index 4c113fc..db8c1c5 100644 --- a/lib/templates/track.xml.erb +++ b/lib/templates/track.xml.erb @@ -1,3 +1,3 @@ -<% each_tag do |name,tag| %><%= "<#{name}>#{tag}\n" %> +<% each_tag do |name,tag| %><%= "<#{name}>#{tag.to_s.encode(:xml => :text)}\n" %> <% end %> \ No newline at end of file From 5298c0fd3b97317bf9b9cdd08a1538d8e4e2e221 Mon Sep 17 00:00:00 2001 From: iainb Date: Thu, 15 Mar 2018 18:38:13 +0000 Subject: [PATCH 34/34] Wasn't conforming properly to XSPF schema, fixed and updated specs. Squashed XML bug in playlist template. --- exe/spfy | 2 +- lib/spfy.rb | 26 ++++--- lib/templates/playlist.xml.erb | 8 +-- spec/spfy_spec.rb | 68 +++++++------------ spec/support/fixtures/all-albums.playlist.xml | 45 ++++-------- .../support/fixtures/mp3-and-mp4.playlist.xml | 27 +++----- spec/support/fixtures/mp3.playlist.xml | 15 ++-- spec/support/fixtures/mp4.m4a.playlist.xml | 6 +- spec/support/fixtures/mp4.m4a.xml | 6 +- 9 files changed, 76 insertions(+), 127 deletions(-) diff --git a/exe/spfy b/exe/spfy index 2ca4007..07e8c8f 100755 --- a/exe/spfy +++ b/exe/spfy @@ -24,7 +24,7 @@ Options: -a NOTE --annotation=NOTE Playlist annotation, default: "Created with Spfy.rb" --no-location Suppress file location output --no-title Suppress track title in output - --no-artist Suppress artist name in output + --no-artist Suppress artist/creator name in track output --no-album Suppress album name in output --no-trackNum Suppress track number in output - CASE SENSITIVE! --max-tracks NUM Limit the output to NUM tracks diff --git a/lib/spfy.rb b/lib/spfy.rb index 53e4048..36b3e09 100755 --- a/lib/spfy.rb +++ b/lib/spfy.rb @@ -19,19 +19,26 @@ module Error; end class Track # The XSPF tags being targeted - XSPF_TAGS = [:location, :album, :artist, :comment, :genre, :title, :trackNum, :year].freeze + XSPF_TAGS = [:location, :album, :creator, :annotation, :title, :trackNum].freeze # The translation of XSPF tags and Taglib tags XSPF_TO_TAGLIB = { - :album => :album, - :artist => :artist, - :comment => :comment, - :genre => :genre, - :title => :title, - :trackNum => :track, - :year => :year, + :album => :album, + :creator => :artist, + :annotation => :comment, + :title => :title, + :trackNum => :track, }.freeze + # Mainly for translating options into XSPF for noes + TAGLIB_TO_XSPF = { + :track => :trackNum, + :title => :title, + :comment => :annotation, + :artist => :creator, + :album => :album, + :location => :location + }.freeze # @param [String,Pathname] path The location of the track. # @param [Hash] options @@ -95,7 +102,8 @@ def respond_to_missing?(name, *) def parse options @noes = options.each_with_object([]){|(k,v), obj| if k =~ /^\-\-no\-/ and v - obj << k.match(/^\-\-no\-(?\w+)$/)[:name].to_sym + taglib_name = k.match(/^\-\-no\-(?\w+)$/)[:name] + obj << TAGLIB_TO_XSPF[taglib_name.to_sym] end obj } diff --git a/lib/templates/playlist.xml.erb b/lib/templates/playlist.xml.erb index 17b35c1..25ae2f0 100644 --- a/lib/templates/playlist.xml.erb +++ b/lib/templates/playlist.xml.erb @@ -1,9 +1,9 @@ - <%= title %> - <%= creator %> - <%= date %> - <%= annotation %> + <%= title.to_s.encode(:xml => :text) %> + <%= creator.to_s.encode(:xml => :text) %> + <%= date.to_s.encode(:xml => :text) %> + <%= annotation.to_s.encode(:xml => :text) %> <%= yield %> diff --git a/spec/spfy_spec.rb b/spec/spfy_spec.rb index af3aa5d..9be8899 100644 --- a/spec/spfy_spec.rb +++ b/spec/spfy_spec.rb @@ -33,72 +33,58 @@ def cmd str Then { track.available_tags.sort == (Spfy::Track::XSPF_TAGS - [:location]).sort } And { track.respond_to?(:location) == false } And { track.respond_to? :album } - And { track.respond_to? :artist } - And { track.respond_to? :comment } - And { track.respond_to? :genre } + And { track.respond_to? :creator } + And { track.respond_to? :annotation } And { track.respond_to? :title } And { track.respond_to? :trackNum } - And { track.respond_to? :year } And { track.respond_to? :data } Then { track.data.respond_to?(:location) == false } And { track.data.respond_to? :album } - And { track.data.respond_to? :artist } - And { track.data.respond_to? :comment } - And { track.data.respond_to? :genre } + And { track.data.respond_to? :creator } + And { track.data.respond_to? :annotation } And { track.data.respond_to? :title } And { track.data.respond_to? :trackNum } - And { track.data.respond_to? :year } Then { track.album == "Album" } - And { track.artist == "Artist" } - And { track.comment == "Comment" } - And { track.genre == "Pop" } + And { track.creator == "Artist" } + And { track.annotation == "Comment" } And { track.title == "Title" } And { track.trackNum == 7 } - And { track.year == 2011 } end end - context "No year or genre" do + context "No title or artist" do Given(:options) { { "--no-location" => false, - "--no-title" => false, - "--no-artist" => false, + "--no-title" => true, + "--no-artist" => true, "--no-album" => false, "--no-tracknum" => false, - "--no-year" => true, - "--no-genre" => true, } } Given(:track) { Spfy::Track.new path, options: options } context "the unit" do - Then { track.instance_variable_get(:@noes).sort == [:genre,:year].sort } + Then { track.instance_variable_get(:@noes).sort == [:creator,:title].sort } - Then { track.available_tags.sort == (Spfy::Track::XSPF_TAGS - [:genre,:year]).sort } + Then { track.available_tags.sort == (Spfy::Track::XSPF_TAGS - [:creator,:title]).sort } And { track.respond_to?(:location) } And { track.respond_to? :album } - And { track.respond_to? :artist } - And { track.respond_to? :comment } - And { track.respond_to?( :genre ) == false } - And { track.respond_to? :title } + And { track.respond_to?( :creator ) == false } + And { track.respond_to? :annotation } + And { track.respond_to?( :title ) == false } And { track.respond_to? :trackNum } - And { track.respond_to?( :year ) == false} And { track.respond_to? :data } Then { track.data.respond_to? :location } And { track.data.respond_to? :album } - And { track.data.respond_to? :artist } - And { track.data.respond_to? :comment } - And { track.data.respond_to?( :genre ) == false } - And { track.data.respond_to? :title } + And { track.data.respond_to?( :creator ) == false } + And { track.data.respond_to? :annotation } + And { track.data.respond_to?( :title ) == false } And { track.data.respond_to? :trackNum } - And { track.data.respond_to?( :year ) == false} Then { track.album == "Album" } - And { track.artist == "Artist" } - And { track.comment == "Comment" } - And { track.title == "Title" } + And { track.annotation == "Comment" } And { track.trackNum == 7 } end end @@ -122,31 +108,25 @@ def cmd str Then { track.available_tags == Spfy::Track::XSPF_TAGS } And { track.respond_to? :location } And { track.respond_to? :album } - And { track.respond_to? :artist } - And { track.respond_to? :comment } - And { track.respond_to? :genre } + And { track.respond_to? :creator } + And { track.respond_to? :annotation } And { track.respond_to? :title } And { track.respond_to? :trackNum } - And { track.respond_to? :year } And { track.respond_to? :data } Then { track.data.respond_to? :location } And { track.data.respond_to? :album } - And { track.data.respond_to? :artist } - And { track.data.respond_to? :comment } - And { track.data.respond_to? :genre } + And { track.data.respond_to? :creator } + And { track.data.respond_to? :annotation } And { track.data.respond_to? :title } And { track.data.respond_to? :trackNum } - And { track.data.respond_to? :year } Then { track.location == location } And { track.album == "Album" } - And { track.artist == "Artist" } - And { track.comment == "Comment" } - And { track.genre == "Pop" } + And { track.creator == "Artist" } + And { track.annotation == "Comment" } And { track.title == "Title" } And { track.trackNum == 7 } - And { track.year == 2011 } end diff --git a/spec/support/fixtures/all-albums.playlist.xml b/spec/support/fixtures/all-albums.playlist.xml index f4147e3..6630566 100644 --- a/spec/support/fixtures/all-albums.playlist.xml +++ b/spec/support/fixtures/all-albums.playlist.xml @@ -8,88 +8,71 @@ ALBUMS/flac/flac.flac Album - Artist - Test file - Pop + Artist + Test file Title 7 - 2011 ALBUMS/mp3/crash.mp3 0 - 0 ALBUMS/mp3/id3v1.mp3 Album - Artist - Comment - Pop + Artist + Comment Title 7 - 2011 ALBUMS/mp3/relative-volume.mp3 0 - 0 ALBUMS/mp3/sample.mp3 Dummy Album - Dummy Artist - Dummy Comment - Pop + Dummy Artist + Dummy Comment Dummy Title 1 - 2000 ALBUMS/mp3/unicode.mp3 你好 0 - 0 ALBUMS/mp4/aiff-sample.aiff AIFF Dummy Album Title - AIFF Dummy Artist Name - AIFF Dummy Comment - Jazz + AIFF Dummy Artist Name + AIFF Dummy Comment AIFF Dummy Track Title - ID3v2.4 3 - 2014 ALBUMS/mp4/mp4.m4a Album - Artist - Comment - Pop + Artist + Comment Title 7 - 2011 ALBUMS/oga/vorbis.oga Album - Artist - Test file - Pop + Artist + Test file Title 7 - 2011 ALBUMS/wav/wav-sample.wav WAV Dummy Album Title - WAV Dummy Artist Name - WAV Dummy Comment - Jazz + WAV Dummy Artist Name + WAV Dummy Comment WAV Dummy Track Title 5 - 2014 \ No newline at end of file diff --git a/spec/support/fixtures/mp3-and-mp4.playlist.xml b/spec/support/fixtures/mp3-and-mp4.playlist.xml index f39b0b4..470a61c 100644 --- a/spec/support/fixtures/mp3-and-mp4.playlist.xml +++ b/spec/support/fixtures/mp3-and-mp4.playlist.xml @@ -8,58 +8,47 @@ ALBUMS/mp3/crash.mp3 0 - 0 ALBUMS/mp3/id3v1.mp3 Album - Artist - Comment - Pop + Artist + Comment Title 7 - 2011 ALBUMS/mp3/relative-volume.mp3 0 - 0 ALBUMS/mp3/sample.mp3 Dummy Album - Dummy Artist - Dummy Comment - Pop + Dummy Artist + Dummy Comment Dummy Title 1 - 2000 ALBUMS/mp3/unicode.mp3 你好 0 - 0 ALBUMS/mp4/aiff-sample.aiff AIFF Dummy Album Title - AIFF Dummy Artist Name - AIFF Dummy Comment - Jazz + AIFF Dummy Artist Name + AIFF Dummy Comment AIFF Dummy Track Title - ID3v2.4 3 - 2014 ALBUMS/mp4/mp4.m4a Album - Artist - Comment - Pop + Artist + Comment Title 7 - 2011 \ No newline at end of file diff --git a/spec/support/fixtures/mp3.playlist.xml b/spec/support/fixtures/mp3.playlist.xml index 213ccaa..6a60643 100644 --- a/spec/support/fixtures/mp3.playlist.xml +++ b/spec/support/fixtures/mp3.playlist.xml @@ -8,38 +8,31 @@ ALBUMS/mp3/crash.mp3 0 - 0 ALBUMS/mp3/id3v1.mp3 Album - Artist - Comment - Pop + Artist + Comment Title 7 - 2011 ALBUMS/mp3/relative-volume.mp3 0 - 0 ALBUMS/mp3/sample.mp3 Dummy Album - Dummy Artist - Dummy Comment - Pop + Dummy Artist + Dummy Comment Dummy Title 1 - 2000 ALBUMS/mp3/unicode.mp3 你好 0 - 0 \ No newline at end of file diff --git a/spec/support/fixtures/mp4.m4a.playlist.xml b/spec/support/fixtures/mp4.m4a.playlist.xml index bfdbcec..1a3b392 100644 --- a/spec/support/fixtures/mp4.m4a.playlist.xml +++ b/spec/support/fixtures/mp4.m4a.playlist.xml @@ -8,12 +8,10 @@ FIXTURE Album - Artist - Comment - Pop + Artist + Comment Title 7 - 2011 \ No newline at end of file diff --git a/spec/support/fixtures/mp4.m4a.xml b/spec/support/fixtures/mp4.m4a.xml index 4151662..d35baa4 100644 --- a/spec/support/fixtures/mp4.m4a.xml +++ b/spec/support/fixtures/mp4.m4a.xml @@ -1,10 +1,8 @@ FIXTURE Album - Artist - Comment - Pop + Artist + Comment Title 7 - 2011 \ No newline at end of file