id3taginator 0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.idea/.gitignore +8 -0
- data/.idea/ID3Taginator.iml +73 -0
- data/.idea/misc.xml +4 -0
- data/.idea/modules.xml +8 -0
- data/.idea/vcs.xml +6 -0
- data/.rspec +3 -0
- data/.rubocop.yml +26 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +12 -0
- data/LICENSE.txt +21 -0
- data/README.md +408 -0
- data/Rakefile +12 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/id3taginator.gemspec +37 -0
- data/lib/id3taginator/audio_file.rb +136 -0
- data/lib/id3taginator/errors/id3_tag_error.rb +12 -0
- data/lib/id3taginator/extensions/argument_check.rb +88 -0
- data/lib/id3taginator/extensions/comparable.rb +28 -0
- data/lib/id3taginator/extensions/encodable.rb +215 -0
- data/lib/id3taginator/extensions/optionable.rb +73 -0
- data/lib/id3taginator/frames/buffer/entities/buffer.rb +26 -0
- data/lib/id3taginator/frames/buffer/rbuf_recommended_buffer_size_frame.rb +59 -0
- data/lib/id3taginator/frames/buffer_frames.rb +32 -0
- data/lib/id3taginator/frames/comment/comm_comment_frame.rb +56 -0
- data/lib/id3taginator/frames/comment/entities/comment.rb +26 -0
- data/lib/id3taginator/frames/comment_frames.rb +42 -0
- data/lib/id3taginator/frames/count/entities/popularimeter.rb +26 -0
- data/lib/id3taginator/frames/count/pcnt_play_counter_frame.rb +40 -0
- data/lib/id3taginator/frames/count/popm_popularimeter_frame.rb +52 -0
- data/lib/id3taginator/frames/count_frames.rb +51 -0
- data/lib/id3taginator/frames/custom_frame.rb +39 -0
- data/lib/id3taginator/frames/custom_frames.rb +54 -0
- data/lib/id3taginator/frames/encryption/aenc_audio_encryption.rb +54 -0
- data/lib/id3taginator/frames/encryption/encr_encryption_method_frame.rb +49 -0
- data/lib/id3taginator/frames/encryption/entities/audio_encryption.rb +28 -0
- data/lib/id3taginator/frames/encryption/entities/encryption_method.rb +26 -0
- data/lib/id3taginator/frames/encryption_frames.rb +77 -0
- data/lib/id3taginator/frames/frameable.rb +75 -0
- data/lib/id3taginator/frames/geo/entities/encapsulated_object.rb +28 -0
- data/lib/id3taginator/frames/geo/geob_general_encapsulated_object_frame.rb +59 -0
- data/lib/id3taginator/frames/geo_frames.rb +41 -0
- data/lib/id3taginator/frames/grouping/entities/group_identification.rb +26 -0
- data/lib/id3taginator/frames/grouping/grid_group_identification_frame.rb +49 -0
- data/lib/id3taginator/frames/grouping/grp1_grouping_frame.rb +40 -0
- data/lib/id3taginator/frames/grouping_frames.rb +61 -0
- data/lib/id3taginator/frames/has_id.rb +51 -0
- data/lib/id3taginator/frames/id3v23_frame_flags.rb +64 -0
- data/lib/id3taginator/frames/id3v24_frame_flags.rb +80 -0
- data/lib/id3taginator/frames/id3v2_frame.rb +249 -0
- data/lib/id3taginator/frames/id3v2_frame_factory.rb +98 -0
- data/lib/id3taginator/frames/ipl/entities/involved_person.rb +24 -0
- data/lib/id3taginator/frames/ipl/ipls_involved_people_frame.rb +48 -0
- data/lib/id3taginator/frames/ipl_frames.rb +31 -0
- data/lib/id3taginator/frames/lyrics/entities/unsync_lyrics.rb +26 -0
- data/lib/id3taginator/frames/lyrics/uslt_unsync_lyrics_frame.rb +56 -0
- data/lib/id3taginator/frames/lyrics_frames.rb +42 -0
- data/lib/id3taginator/frames/mcdi/mcdi_music_cd_identifier_frame.rb +40 -0
- data/lib/id3taginator/frames/mcdi_frames.rb +28 -0
- data/lib/id3taginator/frames/picture/apic_picture_frame.rb +106 -0
- data/lib/id3taginator/frames/picture/entities/picture.rb +32 -0
- data/lib/id3taginator/frames/picture_frames.rb +51 -0
- data/lib/id3taginator/frames/private/entities/private_frame.rb +24 -0
- data/lib/id3taginator/frames/private/priv_private_frame.rb +45 -0
- data/lib/id3taginator/frames/private_frames.rb +40 -0
- data/lib/id3taginator/frames/text/entities/copyright.rb +24 -0
- data/lib/id3taginator/frames/text/entities/date.rb +24 -0
- data/lib/id3taginator/frames/text/entities/part_of_set.rb +24 -0
- data/lib/id3taginator/frames/text/entities/time.rb +24 -0
- data/lib/id3taginator/frames/text/entities/track_number.rb +24 -0
- data/lib/id3taginator/frames/text/entities/user_info.rb +24 -0
- data/lib/id3taginator/frames/text/talb_album_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tbpm_bpm_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tcom_composer_frame.rb +42 -0
- data/lib/id3taginator/frames/text/tcon_genre_frame.rb +104 -0
- data/lib/id3taginator/frames/text/tcop_copyright_frame.rb +55 -0
- data/lib/id3taginator/frames/text/tdat_date_frame.rb +60 -0
- data/lib/id3taginator/frames/text/tdly_playlist_delay_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tenc_encoded_by_frame.rb +40 -0
- data/lib/id3taginator/frames/text/text_writers_frame.rb +43 -0
- data/lib/id3taginator/frames/text/tflt_file_type_frame.rb +71 -0
- data/lib/id3taginator/frames/text/time_time_frame.rb +60 -0
- data/lib/id3taginator/frames/text/tit1_content_group_description_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tit2_title_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tit3_subtitle_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tkey_initial_key_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tlan_language_frame.rb +48 -0
- data/lib/id3taginator/frames/text/tlen_length_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tmed_media_type_frame.rb +40 -0
- data/lib/id3taginator/frames/text/toal_original_album_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tofn_original_filename_frame.rb +40 -0
- data/lib/id3taginator/frames/text/toly_original_writers_frame.rb +43 -0
- data/lib/id3taginator/frames/text/tope_original_artists_frame.rb +43 -0
- data/lib/id3taginator/frames/text/tory_original_release_year_frame.rb +42 -0
- data/lib/id3taginator/frames/text/town_file_owner_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tpe1_artist_frame.rb +43 -0
- data/lib/id3taginator/frames/text/tpe2_album_artist_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tpe3_conductor_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tpe4_modified_by_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tpos_part_of_set_frame.rb +50 -0
- data/lib/id3taginator/frames/text/tpub_publisher_frame.rb +40 -0
- data/lib/id3taginator/frames/text/trck_track_number_frame.rb +50 -0
- data/lib/id3taginator/frames/text/trda_recording_dates_frame.rb +42 -0
- data/lib/id3taginator/frames/text/trsn_internet_radio_station_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tsiz_size_frame.rb +41 -0
- data/lib/id3taginator/frames/text/tsoa_album_sort_order_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tsop_performer_sort_order_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tsot_title_sort_order_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tsrc_isrc_frame.rb +40 -0
- data/lib/id3taginator/frames/text/tsse_encoder_frame.rb +40 -0
- data/lib/id3taginator/frames/text/txxx_user_text_info_frame.rb +51 -0
- data/lib/id3taginator/frames/text/tyer_year_frame.rb +42 -0
- data/lib/id3taginator/frames/text_frames.rb +840 -0
- data/lib/id3taginator/frames/tos/entities/ownership.rb +26 -0
- data/lib/id3taginator/frames/tos/entities/terms_of_use.rb +24 -0
- data/lib/id3taginator/frames/tos/owne_ownership_frame.rb +53 -0
- data/lib/id3taginator/frames/tos/user_terms_of_use_frame.rb +49 -0
- data/lib/id3taginator/frames/tos_frames.rb +54 -0
- data/lib/id3taginator/frames/ufid/entities/ufid_info.rb +24 -0
- data/lib/id3taginator/frames/ufid/ufid_unique_file_identifier_frame.rb +47 -0
- data/lib/id3taginator/frames/ufid_frames.rb +40 -0
- data/lib/id3taginator/frames/url/entities/user_info.rb +24 -0
- data/lib/id3taginator/frames/url/wcom_commercial_url_frame.rb +40 -0
- data/lib/id3taginator/frames/url/wcop_copyright_url_frame.rb +40 -0
- data/lib/id3taginator/frames/url/woaf_official_file_webpage_frame.rb +40 -0
- data/lib/id3taginator/frames/url/woar_official_artist_webpage_frame.rb +40 -0
- data/lib/id3taginator/frames/url/woas_official_source_webpage_frame.rb +40 -0
- data/lib/id3taginator/frames/url/wors_official_radio_station_homepage_frame.rb +40 -0
- data/lib/id3taginator/frames/url/wpay_payment_url_frame.rb +40 -0
- data/lib/id3taginator/frames/url/wpub_official_publisher_webpage_frame.rb +40 -0
- data/lib/id3taginator/frames/url/wxxx_user_url_link_frame.rb +50 -0
- data/lib/id3taginator/frames/url_frames.rb +195 -0
- data/lib/id3taginator/genres.rb +168 -0
- data/lib/id3taginator/header/id3v23_extended_header.rb +37 -0
- data/lib/id3taginator/header/id3v24_extended_header.rb +100 -0
- data/lib/id3taginator/header/id3v2_flags.rb +64 -0
- data/lib/id3taginator/id3v1_tag.rb +156 -0
- data/lib/id3taginator/id3v22_tag.rb +30 -0
- data/lib/id3taginator/id3v23_tag.rb +63 -0
- data/lib/id3taginator/id3v24_tag.rb +75 -0
- data/lib/id3taginator/id3v2_tag.rb +241 -0
- data/lib/id3taginator/options/options.rb +33 -0
- data/lib/id3taginator/util/compress_util.rb +25 -0
- data/lib/id3taginator/util/math_util.rb +68 -0
- data/lib/id3taginator/util/sync_util.rb +65 -0
- data/lib/id3taginator.rb +449 -0
- metadata +198 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Id3Taginator
|
|
4
|
+
module Extensions
|
|
5
|
+
module ArgumentCheck
|
|
6
|
+
def self.included(base)
|
|
7
|
+
base.extend(self)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# raises an ArgumentError if the given argument is nil
|
|
11
|
+
#
|
|
12
|
+
# @param arg [Object] the argument to check
|
|
13
|
+
# @param arg_name [String] the argument name to use in the Error Message
|
|
14
|
+
def argument_not_nil(arg, arg_name)
|
|
15
|
+
raise ArgumentError, "#{arg_name} can't be nil." if arg.nil?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# raises an ArgumentError if the given argument no Symbol
|
|
19
|
+
#
|
|
20
|
+
# @param arg [Object] the argument to check
|
|
21
|
+
# @param arg_name [String] the argument name to use in the Error Message
|
|
22
|
+
def argument_sym(arg, arg_name)
|
|
23
|
+
raise ArgumentError, "#{arg_name} must be sym." unless arg.is_a?(Symbol)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# raises an ArgumentError if the given argument is no Boolean
|
|
27
|
+
#
|
|
28
|
+
# @param arg [Object] the argument to check
|
|
29
|
+
# @param arg_name [String] the argument name to use in the Error Message
|
|
30
|
+
def argument_boolean(arg, arg_name)
|
|
31
|
+
raise ArgumentError, "#{arg_name} must be boolean." unless !!arg == arg
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# raises an ArgumentError if the given argument is no array or empty
|
|
35
|
+
#
|
|
36
|
+
# @param arg [Object] the argument to check
|
|
37
|
+
# @param arg_name [String] the argument name to use in the Error Message
|
|
38
|
+
def argument_not_empty(arg, arg_name)
|
|
39
|
+
raise ArgumentError, "#{arg_name} can't be empty." if arg.nil? || (arg.is_a?(Array) && arg.empty?)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# raises an ArgumentError if the given argument has not the expected number of chars
|
|
43
|
+
#
|
|
44
|
+
# @param arg [String] the argument to check
|
|
45
|
+
# @param arg_name [String] the argument name to use in the Error Message
|
|
46
|
+
# @param num_chars [Integer] number of chars to check against
|
|
47
|
+
def argument_exactly_chars(arg, arg_name, num_chars)
|
|
48
|
+
raise ArgumentError, "#{arg_name} must have #{num_chars} characters." if arg.nil? || arg.length != num_chars
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# rubocop:disable Style/GuardClause
|
|
52
|
+
# raises an ArgumentError if the given argument has not the more than number of chars
|
|
53
|
+
#
|
|
54
|
+
# @param arg [String] the argument to check
|
|
55
|
+
# @param arg_name [String] the argument name to use in the Error Message
|
|
56
|
+
# @param num_chars [Integer] number of chars to check against
|
|
57
|
+
def argument_less_than_chars(arg, arg_name, num_chars)
|
|
58
|
+
if arg.nil? || arg.length > num_chars
|
|
59
|
+
raise ArgumentError, "#{arg_name} must have less than #{num_chars} characters."
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# raises an ArgumentError if the given argument has not the more less number of chars
|
|
64
|
+
#
|
|
65
|
+
# @param arg [String] the argument to check
|
|
66
|
+
# @param arg_name [String] the argument name to use in the Error Message
|
|
67
|
+
# @param num_chars [Integer] number of chars to check against
|
|
68
|
+
def argument_more_than_chars(arg, arg_name, num_chars)
|
|
69
|
+
if arg.nil? || arg.length < num_chars
|
|
70
|
+
raise ArgumentError, "#{arg_name} must have more than #{num_chars} characters."
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# raises an ArgumentError if the given arguments number of chars is not in the given range
|
|
75
|
+
#
|
|
76
|
+
# @param arg [String] the argument to check
|
|
77
|
+
# @param arg_name [String] the argument name to use in the Error Message
|
|
78
|
+
# @param bigger_than [Integer] string must have more than chars
|
|
79
|
+
# @param smaller_than [Integer] string must have less than chars
|
|
80
|
+
def argument_between_num(arg, arg_name, bigger_than, smaller_than)
|
|
81
|
+
if arg.nil? || arg > smaller_than || arg < bigger_than
|
|
82
|
+
raise ArgumentError, "#{arg_name} must have be between #{bigger_than} and #{smaller_than}."
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
# rubocop:enable Style/GuardClause
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Id3Taginator
|
|
4
|
+
module Extensions
|
|
5
|
+
# offers compare methods
|
|
6
|
+
module Comparable
|
|
7
|
+
# compares the instance variables of 2 objects for equality
|
|
8
|
+
# @param obj1 object 1
|
|
9
|
+
# @param obj2 object 2
|
|
10
|
+
# @return [Boolean] true if both instance variables are the same, else false
|
|
11
|
+
def compare(obj1, obj2)
|
|
12
|
+
is_same = true
|
|
13
|
+
obj1.instance_variables.each do |it|
|
|
14
|
+
is_same = false if obj1.instance_variable_get(it) != obj2.instance_variable_get(it)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
is_same
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# compares all Comparables using the defined compare method
|
|
21
|
+
# @param other [Object] the object to compare with
|
|
22
|
+
# @return [Boolean] true if equal, else false
|
|
23
|
+
def ==(other)
|
|
24
|
+
compare(self, other)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Id3Taginator
|
|
4
|
+
module Extensions
|
|
5
|
+
module Encodable
|
|
6
|
+
def self.included(base)
|
|
7
|
+
base.extend(self)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
ZERO = "\x00"
|
|
11
|
+
UNICODE_ZERO = "\x00\x00"
|
|
12
|
+
|
|
13
|
+
ISO8859_1 = 0x00
|
|
14
|
+
UTF_16 = 0x01
|
|
15
|
+
UTF_16BE = 0x02
|
|
16
|
+
UTF_8 = 0x03
|
|
17
|
+
|
|
18
|
+
# returns the ZERO byte depending of the given encoding, so 0x00 0x00 for Unicode, else 0x00
|
|
19
|
+
#
|
|
20
|
+
# @param encoding [Encoding] the encoding to determine the zero byte(s)
|
|
21
|
+
#
|
|
22
|
+
# @return [String] the zero byte(s) String
|
|
23
|
+
def zero_byte(encoding = @options.default_encode_dest)
|
|
24
|
+
encoding == Encoding::ISO8859_1 ? ZERO : UNICODE_ZERO
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# finds the encoding byte for the given encoding
|
|
28
|
+
#
|
|
29
|
+
# @param encoding [Encoding] the encoding
|
|
30
|
+
#
|
|
31
|
+
# @return [Integer] the encoding byte for this encoding
|
|
32
|
+
def find_encoding_byte(encoding)
|
|
33
|
+
case encoding
|
|
34
|
+
when Encoding::ISO8859_1
|
|
35
|
+
ISO8859_1
|
|
36
|
+
when Encoding::UTF_8
|
|
37
|
+
UTF_8
|
|
38
|
+
when Encoding::UTF_16
|
|
39
|
+
UTF_16
|
|
40
|
+
when Encoding::UTF_16BE
|
|
41
|
+
UTF_16BE
|
|
42
|
+
else
|
|
43
|
+
raise Errors::Id3TagError, "#{encoding} is not a valid encoding."
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# finds the encoding for the given encoding Byte
|
|
48
|
+
#
|
|
49
|
+
# @param encoding_byte [Integer] the encoding byte
|
|
50
|
+
#
|
|
51
|
+
# @return [Encoding] the encoding for this encoding byte
|
|
52
|
+
def find_encoding(encoding_byte)
|
|
53
|
+
case encoding_byte
|
|
54
|
+
when ISO8859_1
|
|
55
|
+
Encoding::ISO8859_1
|
|
56
|
+
when UTF_8
|
|
57
|
+
Encoding::UTF_8
|
|
58
|
+
when UTF_16
|
|
59
|
+
Encoding::UTF_16
|
|
60
|
+
when UTF_16BE
|
|
61
|
+
Encoding::UTF_16BE
|
|
62
|
+
else
|
|
63
|
+
raise Errors::Id3TagError, "#{encoding_byte} is not a valid encoding bit."
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# returns the default destination encoding byte
|
|
68
|
+
#
|
|
69
|
+
# @return [String] the String representation of the default destination encoding byte
|
|
70
|
+
def default_encoding_destination_byte
|
|
71
|
+
[find_encoding_byte(@options.default_encode_dest)].pack('C*')
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# reads the given stream until the byte combination is found
|
|
75
|
+
#
|
|
76
|
+
# @param stream [StringIO, IO, File] the stream
|
|
77
|
+
# @param until_bytes [String] read stream until this combination is found
|
|
78
|
+
#
|
|
79
|
+
# @return [String] String representation of the bytes array that is read
|
|
80
|
+
def read_stream_until(stream, until_bytes)
|
|
81
|
+
result = []
|
|
82
|
+
size = until_bytes.length
|
|
83
|
+
until (read = stream.read(size)).nil?
|
|
84
|
+
break if read == until_bytes
|
|
85
|
+
|
|
86
|
+
result << read&.bytes
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
result.flatten.pack('C*')
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# merges the given string representing a byte array to a String representation of the merged byte arrays
|
|
93
|
+
#
|
|
94
|
+
# @param str [Array<String>] byte arrays represented as a String to merge
|
|
95
|
+
#
|
|
96
|
+
# @return [String] String representation of the merged byte arrays (str.bytes)
|
|
97
|
+
def merge(*str)
|
|
98
|
+
str.inject([]) { |sum, x| sum + x.bytes }.pack('C*')
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# decodes the given string using the encoding byte (first byte of the string)
|
|
102
|
+
#
|
|
103
|
+
# @param str [String] String to decode using the first byte to determine the encoding
|
|
104
|
+
#
|
|
105
|
+
# @return [String] the decoded String
|
|
106
|
+
def decode_using_encoding_byte(str)
|
|
107
|
+
encoding_byte = str[0]&.bytes&.first
|
|
108
|
+
raise Errors::Id3TagError, "Could not find encoding byte for String: #{str}" if encoding_byte.nil?
|
|
109
|
+
|
|
110
|
+
encoding = find_encoding(encoding_byte)
|
|
111
|
+
|
|
112
|
+
# noinspection RubyMismatchedParameterType
|
|
113
|
+
decode(str[1..-1], encoding)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# encode the given String from source encoding to destination encoding and adds the destination encoding
|
|
117
|
+
# byte as the first byte
|
|
118
|
+
#
|
|
119
|
+
# @param str [String] the String to encode
|
|
120
|
+
# @param source_encoding [Encoding] the source encoding
|
|
121
|
+
# @param dest_encoding [Encoding] the destination encoding
|
|
122
|
+
#
|
|
123
|
+
# @return [String] the encoded String with the encoding byte as first byte
|
|
124
|
+
def encode_and_add_encoding_byte(str, source_encoding = @options.default_decode_dest,
|
|
125
|
+
dest_encoding = @options.default_encode_dest)
|
|
126
|
+
encoding_byte = find_encoding_byte(dest_encoding)
|
|
127
|
+
result = encode_string(str, source_encoding, dest_encoding)
|
|
128
|
+
result = remove_trailing_zeros(result)
|
|
129
|
+
bytes = result.bytes
|
|
130
|
+
bytes.unshift(encoding_byte)
|
|
131
|
+
|
|
132
|
+
bytes.flatten.pack('C*')
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# decodes the given String from the default destination decoding to the given source encoding
|
|
136
|
+
#
|
|
137
|
+
# @param str [String] the String to decode
|
|
138
|
+
# @param source_encoding [Encoding] the result encoding
|
|
139
|
+
#
|
|
140
|
+
# @return [String] the decoded String
|
|
141
|
+
def decode(str, source_encoding = @options.default_encode_dest)
|
|
142
|
+
result = encode_string(str, source_encoding, @options.default_decode_dest)
|
|
143
|
+
remove_trailing_zeros(result)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# encodes the given String from the default destination encoding to the given source encoding
|
|
147
|
+
#
|
|
148
|
+
# @param str [String] the String to decode
|
|
149
|
+
# @param source_encoding [Encoding] the result encoding
|
|
150
|
+
#
|
|
151
|
+
# @return [String] the encoded String
|
|
152
|
+
def encode(str, source_encoding = @options.default_decode_dest, null_terminated: false)
|
|
153
|
+
encode_string(str, source_encoding, @options.default_encode_dest, null_terminated: null_terminated)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# encodes the given String from source to destination encoding and optionally adds a null termination byte(s)
|
|
157
|
+
#
|
|
158
|
+
# @param str [String] the String to encode
|
|
159
|
+
# @param source_encoding [Encoding] source encoding
|
|
160
|
+
# @param dest_encoding [Encoding] destination encoding
|
|
161
|
+
# @param null_terminated [Boolean] true, if null termination byte(s) should be added
|
|
162
|
+
#
|
|
163
|
+
# @return [String] the encoded String and if requested, added null termination byte(s)
|
|
164
|
+
def encode_string(str, source_encoding, dest_encoding, null_terminated: false)
|
|
165
|
+
if str.empty? && null_terminated && dest_encoding != Encoding::ISO8859_1
|
|
166
|
+
return [0xFE, 0xFF, 0x00, 0x00].pack('C*')
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
res = str.encode(dest_encoding, source_encoding)
|
|
170
|
+
null_terminated ? merge(res, zero_byte(dest_encoding)) : res
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# removes trailing zero bytes if present
|
|
174
|
+
#
|
|
175
|
+
# @param str [String] the string to remove the trailing zeros if present
|
|
176
|
+
#
|
|
177
|
+
# @return [String] the String without trailing zeros
|
|
178
|
+
def remove_trailing_zeros(str)
|
|
179
|
+
return str if str.empty?
|
|
180
|
+
|
|
181
|
+
last = str.bytes.last
|
|
182
|
+
while last&.zero?
|
|
183
|
+
str.chop!
|
|
184
|
+
last = str.bytes.last
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
str
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# pads the given String with X chars to the left
|
|
191
|
+
#
|
|
192
|
+
# @param str [String] the String to pad
|
|
193
|
+
# @param padding_to [Integer] pad until length of
|
|
194
|
+
# @param char [String] the char to use to pad
|
|
195
|
+
#
|
|
196
|
+
# @return [String] the padded String
|
|
197
|
+
def pad_left(str, padding_to, char = 0.chr)
|
|
198
|
+
str ||= ''
|
|
199
|
+
str.ljust(padding_to, char)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# pads the given String with X chars to the right
|
|
203
|
+
#
|
|
204
|
+
# @param str [String] the String to pad
|
|
205
|
+
# @param padding_to [Integer] pad until length of
|
|
206
|
+
# @param char [String] the char to use to pad
|
|
207
|
+
#
|
|
208
|
+
# @return [String] the padded String
|
|
209
|
+
def pad_right(str, padding_to, char = 0.chr)
|
|
210
|
+
str ||= ''
|
|
211
|
+
str.rjust(padding_to, char)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Id3Taginator
|
|
4
|
+
module Extensions
|
|
5
|
+
module Optionable
|
|
6
|
+
# sets the default encoding for destination
|
|
7
|
+
#
|
|
8
|
+
# @param encoding [Encoding] the encoding to set
|
|
9
|
+
#
|
|
10
|
+
# @return [Self] returns self for builder style
|
|
11
|
+
def default_encode_for_destination(encoding)
|
|
12
|
+
raise Errors::Id3TagError, 'Encoding can\'t be nil' if encoding.nil?
|
|
13
|
+
|
|
14
|
+
@options.default_encode_dest = encoding
|
|
15
|
+
self
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# sets the default decoding for destination
|
|
19
|
+
#
|
|
20
|
+
# @param encoding [Encoding] the encoding to set
|
|
21
|
+
#
|
|
22
|
+
# @return [Self] returns self for builder style
|
|
23
|
+
def default_decode_for_destination(encoding)
|
|
24
|
+
raise Errors::Id3TagError, 'Encoding can\'t be nil' if encoding.nil?
|
|
25
|
+
|
|
26
|
+
@options.default_decode_dest = encoding
|
|
27
|
+
self
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# sets the padding
|
|
31
|
+
#
|
|
32
|
+
# @param number_bytes [Integer] number of bytes to pad
|
|
33
|
+
#
|
|
34
|
+
# @return [Self] returns self for builder style
|
|
35
|
+
def tag_padding(number_bytes)
|
|
36
|
+
number_bytes = 0 if number_bytes.nil?
|
|
37
|
+
@options.padding_bytes = number_bytes
|
|
38
|
+
self
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# if true, does not throw an error if a frame is added that is not supported for 2.3 e.g. Sort Order Frames
|
|
42
|
+
#
|
|
43
|
+
# @param ignore [Boolean] true to ignore errors, else false
|
|
44
|
+
#
|
|
45
|
+
# @return [Self] returns self for builder style
|
|
46
|
+
def ignore_v23_frame_error(ignore = true)
|
|
47
|
+
@options.ignore_v23_frame_error = ignore
|
|
48
|
+
self
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# if true, does not throw an error if a frame is added that is not supported for 2.4 e.g. Size Frame
|
|
52
|
+
#
|
|
53
|
+
# @param ignore [Boolean] true to ignore errors, else false
|
|
54
|
+
#
|
|
55
|
+
# @return [Self] returns self for builder style
|
|
56
|
+
def ignore_v24_frame_error(ignore = true)
|
|
57
|
+
@options.ignore_v24_frame_error = ignore
|
|
58
|
+
self
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# if true, the size frame TSIZ will be added automatically to v2.3 if not present. Will be added to v2.4 if
|
|
62
|
+
# ignore_v24_frame_error is true
|
|
63
|
+
|
|
64
|
+
# @param add_frame [Boolean] true to add the frame automatically if not present, else false
|
|
65
|
+
#
|
|
66
|
+
# @return [Self] returns self for builder style
|
|
67
|
+
def add_size_frame(add_frame = true)
|
|
68
|
+
@options.add_size_frame = add_frame
|
|
69
|
+
self
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Id3Taginator
|
|
4
|
+
module Frames
|
|
5
|
+
module Buffer
|
|
6
|
+
module Entities
|
|
7
|
+
class Buffer
|
|
8
|
+
include Extensions::Comparable
|
|
9
|
+
|
|
10
|
+
attr_accessor :buffer_size, :embedded_info_flag, :offset_next_tag
|
|
11
|
+
|
|
12
|
+
# constructor
|
|
13
|
+
#
|
|
14
|
+
# @param buffer_size [Integer] the buffer size
|
|
15
|
+
# @param embedded_info_flag [Boolean] true if infos are embedded
|
|
16
|
+
# @param offset_next_tag [Integer, nil] offset till next tag starts in bytes
|
|
17
|
+
def initialize(buffer_size, embedded_info_flag, offset_next_tag)
|
|
18
|
+
@buffer_size = buffer_size
|
|
19
|
+
@embedded_info_flag = embedded_info_flag
|
|
20
|
+
@offset_next_tag = offset_next_tag
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Id3Taginator
|
|
4
|
+
module Frames
|
|
5
|
+
module Buffer
|
|
6
|
+
class RecommendedBufferSizeFrame < Id3v2Frame
|
|
7
|
+
include HasId
|
|
8
|
+
|
|
9
|
+
frame_info :BUF, :RBUF, :RBUF
|
|
10
|
+
|
|
11
|
+
attr_accessor :buffer_size, :embedded_info_flag, :offset_next_tag
|
|
12
|
+
|
|
13
|
+
# builds the recommended buffer size frame
|
|
14
|
+
#
|
|
15
|
+
# @param buffer_size [Integer] the buffer size
|
|
16
|
+
# @param embedded_info_flag [Boolean] true if infos are embedded
|
|
17
|
+
# @param offset_next_tag [Integer] offset till next tag starts in bytes
|
|
18
|
+
# @param options [Options::Options] options to use
|
|
19
|
+
# @param id3_version [Integer] the id3 version to build the frame for
|
|
20
|
+
#
|
|
21
|
+
# @return [Id3v2Frame] the resulting id3v2 frame
|
|
22
|
+
def self.build_frame(buffer_size, embedded_info_flag, offset_next_tag, options = nil, id3_version: 3)
|
|
23
|
+
supported?('RBUF', id3_version, options)
|
|
24
|
+
|
|
25
|
+
argument_not_nil(buffer_size, 'buffer_size')
|
|
26
|
+
argument_not_nil(embedded_info_flag, 'embedded_into_flag')
|
|
27
|
+
argument_boolean(embedded_info_flag, 'embedded_into_flag')
|
|
28
|
+
|
|
29
|
+
frame = new(frame_id(id3_version, options), 0, build_id3_flags(id3_version), '')
|
|
30
|
+
frame.buffer_size = buffer_size
|
|
31
|
+
frame.embedded_info_flag = embedded_info_flag
|
|
32
|
+
frame.offset_next_tag = offset_next_tag
|
|
33
|
+
frame
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def process_content(content)
|
|
37
|
+
stream = StringIO.new(content)
|
|
38
|
+
# noinspection RubyNilAnalysis
|
|
39
|
+
@buffer_size = Util::MathUtil.to_number(stream.read(3).bytes)
|
|
40
|
+
@embedded_info_flag = (stream.readbyte & 0b00000001) == 0b00000001
|
|
41
|
+
offset_nxt_tag = stream.read(4)
|
|
42
|
+
@offset_next_tag = offset_nxt_tag.nil? ? nil : offset_nxt_tag.bytes.pack('C*').unpack1('N')
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def content_to_bytes
|
|
46
|
+
buffer_size = @buffer_size.to_s(16).rjust(6, '0').scan(/../).map { |x| x.hex.chr }.join
|
|
47
|
+
embedded_into = @embedded_info_flag ? "\x01" : "\x00"
|
|
48
|
+
offset_next_tag = if @offset_next_tag.nil?
|
|
49
|
+
nil
|
|
50
|
+
else
|
|
51
|
+
@offset_next_tag.to_s(16).rjust(8, '0').scan(/../).map { |x| x.hex.chr }.join
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
offset_next_tag.nil? ? merge(buffer_size, embedded_into) : merge(buffer_size, embedded_into, offset_next_tag)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Id3Taginator
|
|
4
|
+
module Frames
|
|
5
|
+
module BufferFrames
|
|
6
|
+
include Frames::Frameable
|
|
7
|
+
|
|
8
|
+
# extracts the recommended buffer size (RBUF/BUF)
|
|
9
|
+
#
|
|
10
|
+
# @return [Frames::Buffer::Entities::Buffer, nil] returns the Buffer
|
|
11
|
+
def recommended_buffer_size
|
|
12
|
+
frame = find_frame(Buffer::RecommendedBufferSizeFrame.frame_id(@major_version, @options))
|
|
13
|
+
return nil if frame.nil?
|
|
14
|
+
|
|
15
|
+
Frames::Buffer::Entities::Buffer.new(frame.buffer_size, frame.embedded_info_flag, frame.offset_next_tag)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# sets the recommended buffer size (RBUF/BUF)
|
|
19
|
+
#
|
|
20
|
+
# @param buffer [Frames::Buffer::Entities::Buffer] the buffer
|
|
21
|
+
def recommended_buffer_size=(buffer)
|
|
22
|
+
set_frame_fields(Buffer::RecommendedBufferSizeFrame, %i[@buffer_size @embedded_info_flag @offset_next_tag],
|
|
23
|
+
buffer.buffer_size, buffer.embedded_info_flag, buffer.offset_next_tag)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# removes the recommended buffer size frame
|
|
27
|
+
def remove_recommended_buffer_size
|
|
28
|
+
@frames.delete_if { |f| f.frame_id == Buffer::RecommendedBufferSizeFrame.frame_id(@major_version, @options) }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Id3Taginator
|
|
4
|
+
module Frames
|
|
5
|
+
module Comment
|
|
6
|
+
class CommentFrame < Id3v2Frame
|
|
7
|
+
include HasId
|
|
8
|
+
|
|
9
|
+
frame_info :COM, :COMM, :COMM
|
|
10
|
+
|
|
11
|
+
attr_accessor :language, :descriptor, :text
|
|
12
|
+
|
|
13
|
+
# builds the comment frame
|
|
14
|
+
#
|
|
15
|
+
# @param language [String] the language of the comment (3 characters)
|
|
16
|
+
# @param descriptor [String] description
|
|
17
|
+
# @param text [String] the comment text
|
|
18
|
+
# @param options [Options::Options] options to use
|
|
19
|
+
# @param id3_version [Integer] the id3 version to build the frame for
|
|
20
|
+
#
|
|
21
|
+
# @return [Id3v2Frame] the resulting id3v2 frame
|
|
22
|
+
def self.build_frame(language, descriptor, text, options = nil, id3_version: 3)
|
|
23
|
+
supported?('COMM', id3_version, options)
|
|
24
|
+
|
|
25
|
+
argument_not_nil(language, 'language')
|
|
26
|
+
argument_exactly_chars(language, 'language', 3)
|
|
27
|
+
argument_not_nil(text, 'text')
|
|
28
|
+
descriptor ||= ''
|
|
29
|
+
|
|
30
|
+
frame = new(frame_id(id3_version, options), 0, build_id3_flags(id3_version), '')
|
|
31
|
+
frame.language = language
|
|
32
|
+
frame.descriptor = descriptor
|
|
33
|
+
frame.text = text
|
|
34
|
+
frame
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def process_content(content)
|
|
38
|
+
stream = StringIO.new(content)
|
|
39
|
+
encoding = find_encoding(stream.readbyte)
|
|
40
|
+
|
|
41
|
+
@language = stream.read(3)
|
|
42
|
+
encoded_descriptor = read_stream_until(stream, zero_byte(encoding))
|
|
43
|
+
@descriptor = decode(encoded_descriptor, encoding)
|
|
44
|
+
encoded_text = stream.read
|
|
45
|
+
@text = decode(encoded_text, encoding)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def content_to_bytes
|
|
49
|
+
encoded_descriptor = encode(@descriptor, null_terminated: true)
|
|
50
|
+
encoded_text = encode(@text)
|
|
51
|
+
merge(default_encoding_destination_byte, @language, encoded_descriptor, encoded_text)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Id3Taginator
|
|
4
|
+
module Frames
|
|
5
|
+
module Comment
|
|
6
|
+
module Entities
|
|
7
|
+
class Comment
|
|
8
|
+
include Extensions::Comparable
|
|
9
|
+
|
|
10
|
+
attr_accessor :language, :descriptor, :text
|
|
11
|
+
|
|
12
|
+
# constructor
|
|
13
|
+
#
|
|
14
|
+
# @param language [String] the language of the comment (3 characters)
|
|
15
|
+
# @param descriptor [String] description
|
|
16
|
+
# @param text [String] the comment text
|
|
17
|
+
def initialize(language, descriptor, text)
|
|
18
|
+
@language = language
|
|
19
|
+
@descriptor = descriptor
|
|
20
|
+
@text = text
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Id3Taginator
|
|
4
|
+
module Frames
|
|
5
|
+
module CommentFrames
|
|
6
|
+
include Frames::Frameable
|
|
7
|
+
|
|
8
|
+
# extracts the comments (COMM/COM)
|
|
9
|
+
#
|
|
10
|
+
# @return [Array<Frames::Comment::Entities::Comment>] returns the Comments
|
|
11
|
+
def comments
|
|
12
|
+
frame = find_frames(Comment::CommentFrame.frame_id(@major_version, @options))
|
|
13
|
+
return [] if frame.nil? || frame.empty?
|
|
14
|
+
|
|
15
|
+
frame.map { |f| Comment::Entities::Comment.new(f.language, f.descriptor, f.text) }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# adds a comment (COMM/COM)
|
|
19
|
+
# Multiple ones can be added, as long as they have different language/descriptor
|
|
20
|
+
#
|
|
21
|
+
# @param comment [Frames::Comment::Entities::Comment] the comment to set
|
|
22
|
+
def comment=(comment)
|
|
23
|
+
set_frame_fields_by_selector(Comment::CommentFrame, %i[@language @descriptor @text],
|
|
24
|
+
->(f) { f.language == comment.language && f.descriptor == comment.descriptor },
|
|
25
|
+
comment.language, comment.descriptor, comment.text)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
alias add_comment comment=
|
|
29
|
+
|
|
30
|
+
# removes a comment for the specific language and descriptor
|
|
31
|
+
#
|
|
32
|
+
# @param language [String] the language
|
|
33
|
+
# @param descriptor [String] the descriptor
|
|
34
|
+
def remove_comment(language, descriptor)
|
|
35
|
+
@frames.delete_if do |f|
|
|
36
|
+
f.frame_id == Comment::CommentFrame.frame_id(@major_version, @options) && f.language == language &&
|
|
37
|
+
f.descriptor == descriptor
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Id3Taginator
|
|
4
|
+
module Frames
|
|
5
|
+
module Count
|
|
6
|
+
module Entities
|
|
7
|
+
class Popularimeter
|
|
8
|
+
include Extensions::Comparable
|
|
9
|
+
|
|
10
|
+
attr_accessor :email, :rating, :counter
|
|
11
|
+
|
|
12
|
+
# constructor
|
|
13
|
+
#
|
|
14
|
+
# @param email [String] email of the user
|
|
15
|
+
# @param rating [Integer] the rating between 0 and 255
|
|
16
|
+
# @param counter [Integer] the counter, default 32 bit integer, but can be higher too
|
|
17
|
+
def initialize(email, rating, counter)
|
|
18
|
+
@email = email
|
|
19
|
+
@rating = rating
|
|
20
|
+
@counter = counter
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|