id3taginator 0.8
Sign up to get free protection for your applications and to get access to all the features.
- 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,156 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Id3Taginator
|
4
|
+
class Id3v1Tag
|
5
|
+
include Extensions::Encodable
|
6
|
+
|
7
|
+
IDENTIFIER = 'TAG'
|
8
|
+
TAG_SIZE = 128
|
9
|
+
|
10
|
+
attr_reader :title, :artist, :album, :year, :comment, :track, :genre
|
11
|
+
|
12
|
+
# checks if the given stream contains a id3v1 tag
|
13
|
+
#
|
14
|
+
# @param file [StringIO, IO, File] the file stream to check
|
15
|
+
#
|
16
|
+
# @return [Boolean] true if contains an id3v1 tag, else false
|
17
|
+
def self.id3v1_tag?(file)
|
18
|
+
tag = file.read(3)
|
19
|
+
file.seek(-3, IO::SEEK_CUR)
|
20
|
+
|
21
|
+
tag == IDENTIFIER
|
22
|
+
end
|
23
|
+
|
24
|
+
# builds an id3tag from the given file stream
|
25
|
+
#
|
26
|
+
# @param file [StringIO, IO, File] the file stream
|
27
|
+
#
|
28
|
+
# @return [Id3v1Tag] the id3v1tag object
|
29
|
+
def self.build_from_file(file)
|
30
|
+
tag = file.read(3)
|
31
|
+
unless tag == IDENTIFIER
|
32
|
+
raise Errors::Id3TagError, "#{tag} is no valid Id3v1 identifier. The Tag seems to be corrupted."
|
33
|
+
end
|
34
|
+
|
35
|
+
title = file.read(30)&.strip
|
36
|
+
artist = file.read(30)&.strip
|
37
|
+
album = file.read(30)&.strip
|
38
|
+
year = file.read(4)
|
39
|
+
comment = file.read(28)
|
40
|
+
track_flag = file.readbyte
|
41
|
+
track = nil
|
42
|
+
if track_flag.zero?
|
43
|
+
track = file.readbyte
|
44
|
+
else
|
45
|
+
comment += track_flag.chr + file.readbyte.chr
|
46
|
+
end
|
47
|
+
comment = comment&.strip
|
48
|
+
genre = Genres.genre(file.readbyte)
|
49
|
+
|
50
|
+
new(title, artist, album, year, comment, track, genre)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Constructor
|
54
|
+
#
|
55
|
+
# @param title [String, nil] the title
|
56
|
+
# @param artist [String, nil] the artist
|
57
|
+
# @param album [String, nil] the album title
|
58
|
+
# @param year [Integer, String, nil] the year
|
59
|
+
# @param comment [String, nil] the comment
|
60
|
+
# @param track [Integer, nil] the track number
|
61
|
+
# @param genre [Symbol, nil] the genre name as a Symbol, e.g. :ROCK
|
62
|
+
def initialize(title = nil, artist = nil, album = nil, year = nil, comment = nil, track = nil, genre = :INVALID)
|
63
|
+
@title = title
|
64
|
+
@artist = artist
|
65
|
+
@album = album
|
66
|
+
@year = year
|
67
|
+
@comment = comment
|
68
|
+
@track = track
|
69
|
+
@genre = genre
|
70
|
+
end
|
71
|
+
|
72
|
+
# sets a title, up to 30 characters
|
73
|
+
#
|
74
|
+
# @param title [String] the title
|
75
|
+
def title=(title)
|
76
|
+
raise Errors::Id3TagError, 'Title can\'t be longer than 30 characters.' if title.length > 30
|
77
|
+
|
78
|
+
@title = title
|
79
|
+
end
|
80
|
+
|
81
|
+
# sets an artist, up to 30 characters
|
82
|
+
#
|
83
|
+
# @param artist [String] the artist
|
84
|
+
def artist=(artist)
|
85
|
+
raise Errors::Id3TagError, 'Artist can\'t be longer than 30 characters.' if artist.length > 30
|
86
|
+
|
87
|
+
@artist = artist
|
88
|
+
end
|
89
|
+
|
90
|
+
# sets an album title, up to 30 characters
|
91
|
+
#
|
92
|
+
# @param album [String] the album title
|
93
|
+
def album=(album)
|
94
|
+
raise Errors::Id3TagError, 'Album can\'t be longer than 30 characters.' if album.length > 30
|
95
|
+
|
96
|
+
@album = album
|
97
|
+
end
|
98
|
+
|
99
|
+
# sets a year, exactly 4 characters, e.g. 2021
|
100
|
+
#
|
101
|
+
# @param year [Integer, String] the year
|
102
|
+
def year=(year)
|
103
|
+
year = year.to_s
|
104
|
+
raise Errors::Id3TagError, 'Year must be 4 characters.' if year.length != 4
|
105
|
+
|
106
|
+
@year = year
|
107
|
+
end
|
108
|
+
|
109
|
+
# sets a comment, up to 30 characters
|
110
|
+
#
|
111
|
+
# @param comment [String] the comment
|
112
|
+
def comment=(comment)
|
113
|
+
raise Errors::Id3TagError, 'Comment can\'t be longer than 30 characters.' if comment.length > 30
|
114
|
+
|
115
|
+
@comment = comment
|
116
|
+
end
|
117
|
+
|
118
|
+
# sets a genre, this must be a SYM from the Genre list in Genres, e.g. :ROCK
|
119
|
+
#
|
120
|
+
# @param genre_name [Symbol] the genre name as symbol, e.g. :ROCK
|
121
|
+
def genre=(genre_name)
|
122
|
+
@genre = genre_name.to_sym
|
123
|
+
end
|
124
|
+
|
125
|
+
# sets a track number
|
126
|
+
#
|
127
|
+
# @param track [Integer] the track, 0 < track# <= 255
|
128
|
+
def track=(track)
|
129
|
+
track = track.to_i
|
130
|
+
raise Errors::Id3TagError, 'Track must be > 0 and < 255.' if !track.nil? && (track.negative? || track > 255)
|
131
|
+
|
132
|
+
@track = track
|
133
|
+
end
|
134
|
+
|
135
|
+
# dumps the id3v1 tag to a string representing the bytes
|
136
|
+
#
|
137
|
+
# @return [String] id3v1 byte dump as a string. tag.bytes returns the bytes of the dump
|
138
|
+
def to_bytes
|
139
|
+
res = 'TAG'
|
140
|
+
res += pad_left(@title.nil? ? '' : @title, 30)
|
141
|
+
res += pad_left(@artist.nil? ? '' : @artist, 30)
|
142
|
+
res += pad_left(@album.nil? ? '' : @album, 30)
|
143
|
+
res += pad_left(@year.nil? ? '' : @year&.to_s, 4)
|
144
|
+
if @track.nil?
|
145
|
+
res += pad_left(@comment.nil? ? '' : @comment, 30)
|
146
|
+
else
|
147
|
+
res += pad_left(@comment.nil? ? '' : @comment, 28)
|
148
|
+
res += 0.chr
|
149
|
+
res += @track.chr
|
150
|
+
end
|
151
|
+
res += Genres.genre_by_name(@genre).chr
|
152
|
+
|
153
|
+
res
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Id3Taginator
|
4
|
+
class Id3v22Tag < Id3v2Tag
|
5
|
+
|
6
|
+
HEADER_SIZE = 10
|
7
|
+
|
8
|
+
# builds an empty Id3v2.2 tag with the given options set
|
9
|
+
#
|
10
|
+
# @param options [Options::Options] the options to use
|
11
|
+
#
|
12
|
+
# @return [Id3v22Tag] returns an empty Id3v2.2 tag
|
13
|
+
def self.build_empty(options)
|
14
|
+
new(0, Header::Id3v2Flags.new(0x00), 0, StringIO.new, options)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Constructor
|
18
|
+
#
|
19
|
+
# @param minor_version [Integer] the minor version, in v2.[major].[minor]
|
20
|
+
# @param flags [Header::Id3v2Flags] the 2 Byte header flags wrapped in the entity
|
21
|
+
# @param tag_size [Integer] number of bytes the excluding header/footer of the tag
|
22
|
+
# @param tag_data_stream [StringIO|IO|File] the file stream
|
23
|
+
# @param options [Options::Options] the options to use
|
24
|
+
def initialize(minor_version, flags, tag_size, tag_data_stream, options)
|
25
|
+
@options = options
|
26
|
+
frames = tag_size.positive? ? parse_frames(tag_data_stream, 2) : []
|
27
|
+
super(HEADER_SIZE, 2, minor_version, flags, tag_size, frames, nil)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Id3Taginator
|
4
|
+
class Id3v23Tag < Id3v2Tag
|
5
|
+
|
6
|
+
HEADER_SIZE = 10
|
7
|
+
|
8
|
+
# builds an empty Id3v232 tag with the given options set
|
9
|
+
#
|
10
|
+
# @param options [Options::Options] the options to use
|
11
|
+
#
|
12
|
+
# @return [Id3v23Tag] returns an empty Id3v2.3 tag
|
13
|
+
def self.build_empty(options)
|
14
|
+
new(0, Header::Id3v2Flags.new(0x00), 0, StringIO.new, options)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Constructor
|
18
|
+
#
|
19
|
+
# @param minor_version [Integer] the minor version, in v2.[major].[minor]
|
20
|
+
# @param flags [Header::Id3v2Flags] the 2 Byte header flags wrapped in the entity
|
21
|
+
# @param tag_size [Integer] number of bytes the excluding header/footer of the tag
|
22
|
+
# @param tag_data_stream [StringIO, IO, File] the file stream
|
23
|
+
# @param options [Options::Options] the options to use
|
24
|
+
def initialize(minor_version, flags, tag_size, tag_data_stream, options)
|
25
|
+
@options = options
|
26
|
+
ext_header = flags.extended_header? && tag_size.positive? ? parse_extended_header(tag_data_stream) : nil
|
27
|
+
frames = tag_size.positive? ? parse_frames(tag_data_stream, 3) : []
|
28
|
+
super(HEADER_SIZE, 3, minor_version, flags, tag_size, frames, ext_header)
|
29
|
+
end
|
30
|
+
|
31
|
+
# parses the extended header if present
|
32
|
+
#
|
33
|
+
# @param file [StringIO, IO, File] the file stream
|
34
|
+
#
|
35
|
+
# @return [Header::Id3v23ExtendedHeader] the extended header
|
36
|
+
def parse_extended_header(file)
|
37
|
+
size = Util::MathUtil.to_number(file.read(4)&.bytes)
|
38
|
+
flags = file.read(2)
|
39
|
+
|
40
|
+
raise Errors::Id3TagError, 'Could not find extended header flag bytes. ID3v2 Tag is corrupt.' if flags.nil?
|
41
|
+
|
42
|
+
padding = Util::MathUtil.to_number(file.read(4)&.bytes)
|
43
|
+
|
44
|
+
ext_header = Header::Id3v23ExtendedHeader.new(size, flags, padding)
|
45
|
+
ext_header.crc_data = file.read(4) if ext_header.crc?
|
46
|
+
ext_header
|
47
|
+
end
|
48
|
+
|
49
|
+
# determines if an extended header is present
|
50
|
+
#
|
51
|
+
# @return [Boolean] true if header is present, else false
|
52
|
+
def extended_header?
|
53
|
+
@flags.extended_header?
|
54
|
+
end
|
55
|
+
|
56
|
+
# determines if experimental tags are present
|
57
|
+
#
|
58
|
+
# @return [Boolean] true if experimental tags present, else false
|
59
|
+
def experimental?
|
60
|
+
@flags.experimental?
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Id3Taginator
|
4
|
+
class Id3v24Tag < Id3v2Tag
|
5
|
+
|
6
|
+
HEADER_SIZE = 10
|
7
|
+
|
8
|
+
# builds an empty Id3v2.4 tag with the given options set
|
9
|
+
#
|
10
|
+
# @param options [Options::Options] the options to use
|
11
|
+
#
|
12
|
+
# @return [Id3v24Tag] returns an empty Id3v2.4 tag
|
13
|
+
def self.build_empty(options)
|
14
|
+
new(0, Header::Id3v2Flags.new(0x00), 0, StringIO.new, options)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Constructor
|
18
|
+
#
|
19
|
+
# @param minor_version [Integer] the minor version, in v2.[major].[minor]
|
20
|
+
# @param flags [Header::Id3v2Flags] the 2 Byte header flags wrapped in the entity
|
21
|
+
# @param tag_size [Integer] number of bytes the excluding header/footer of the tag
|
22
|
+
# @param tag_data_stream [StringIO, IO, File] the file stream
|
23
|
+
# @param options [Options::Options] the options to use
|
24
|
+
def initialize(minor_version, flags, tag_size, tag_data_stream, options)
|
25
|
+
@options = options
|
26
|
+
ext_header = flags.extended_header? && tag_size.positive? ? parse_extended_header(tag_data_stream) : nil
|
27
|
+
|
28
|
+
frames = tag_size.positive? ? parse_frames(tag_data_stream, 4) : []
|
29
|
+
super(HEADER_SIZE, 4, minor_version, flags, tag_size, frames, ext_header)
|
30
|
+
end
|
31
|
+
|
32
|
+
# parses the extended header if present
|
33
|
+
#
|
34
|
+
# @param file [StringIO, IO, File] the file stream
|
35
|
+
#
|
36
|
+
# @return [Header::Id3v24ExtendedHeader] the extended header
|
37
|
+
def parse_extended_header(file)
|
38
|
+
size = Util::MathUtil.to_32_synchsafe_integer(file.read(4)&.bytes)
|
39
|
+
number_of_flags = file.readbyte
|
40
|
+
flags = file.read(number_of_flags)
|
41
|
+
|
42
|
+
raise Errors::Id3TagError, 'Could not find extended header flag bytes. ID3v2 Tag is corrupt.' if flags.nil?
|
43
|
+
|
44
|
+
Header::Id3v24ExtendedHeader.new(size, flags)
|
45
|
+
end
|
46
|
+
|
47
|
+
# determines if an extended header is present
|
48
|
+
#
|
49
|
+
# @return [Boolean] true if header is present, else false
|
50
|
+
def extended_header?
|
51
|
+
@flags.extended_header?
|
52
|
+
end
|
53
|
+
|
54
|
+
# determines if experimental tags are present
|
55
|
+
#
|
56
|
+
# @return [Boolean] true if experimental tags present, else false
|
57
|
+
def experimental?
|
58
|
+
@flags.experimental?
|
59
|
+
end
|
60
|
+
|
61
|
+
# determines if a footer is present
|
62
|
+
#
|
63
|
+
# @return [Boolean] true if a footer is present, else false
|
64
|
+
def footer?
|
65
|
+
@flags.footer?
|
66
|
+
end
|
67
|
+
|
68
|
+
# enables or disables a footer
|
69
|
+
#
|
70
|
+
# @param enabled [Boolean] true, if the footer should be set to the tag, else false
|
71
|
+
def footer=(enabled = true)
|
72
|
+
@flags.footer = enabled
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,241 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Id3Taginator
|
4
|
+
class Id3v2Tag
|
5
|
+
include Frames::UfidFrames
|
6
|
+
include Frames::TextFrames
|
7
|
+
include Frames::UrlFrames
|
8
|
+
include Frames::IplFrames
|
9
|
+
include Frames::McdiFrames
|
10
|
+
include Frames::LyricsFrames
|
11
|
+
include Frames::CommentFrames
|
12
|
+
include Frames::PictureFrames
|
13
|
+
include Frames::GeoFrames
|
14
|
+
include Frames::CountFrames
|
15
|
+
include Frames::BufferFrames
|
16
|
+
include Frames::ToSFrames
|
17
|
+
include Frames::EncryptionFrames
|
18
|
+
include Frames::GroupingFrames
|
19
|
+
include Frames::PrivateFrames
|
20
|
+
include Frames::CustomFrames
|
21
|
+
|
22
|
+
IDENTIFIER = 'ID3'
|
23
|
+
|
24
|
+
attr_writer :options
|
25
|
+
|
26
|
+
# checks if the given stream contains a id3v2 tag
|
27
|
+
#
|
28
|
+
# @param file [StringIO, IO, File] the file stream to check
|
29
|
+
#
|
30
|
+
# @return [Boolean] true if contains an id3v2 tag, else false
|
31
|
+
def self.id3v2_tag?(file)
|
32
|
+
tag = file.read(3)
|
33
|
+
file.seek(0)
|
34
|
+
|
35
|
+
tag == IDENTIFIER
|
36
|
+
end
|
37
|
+
|
38
|
+
# builds an id3tag from the given file stream and the passed options
|
39
|
+
#
|
40
|
+
# @param file [StringIO, IO, File] the file stream
|
41
|
+
# @param options [Options::Options] options that should be used
|
42
|
+
#
|
43
|
+
# @return [Id3v2Tag] the id3v2tag object,the specific object is determined by the tag version
|
44
|
+
def self.build_from_file(file, options)
|
45
|
+
file.seek(0)
|
46
|
+
tag = file.read(3)
|
47
|
+
raise Errors::Id3TagError, "#{tag} is no valid ID3v2 tag. Tag seems corrupted." unless tag == IDENTIFIER
|
48
|
+
|
49
|
+
major_version = file.readbyte
|
50
|
+
minor_version = file.readbyte
|
51
|
+
flags = Header::Id3v2Flags.new(file.readbyte)
|
52
|
+
tag_size = Util::MathUtil.to_32_synchsafe_integer(file.read(4)&.bytes)
|
53
|
+
|
54
|
+
tag_data = file.read(tag_size)
|
55
|
+
raise Errors::Id3TagError, 'There is no Tag data to read. ID3v2 tag seems to be invalid.' if tag_data.nil?
|
56
|
+
|
57
|
+
tag_data = Util::SyncUtil.undo_synchronization(StringIO.new(tag_data)) if flags.unsynchronized?
|
58
|
+
|
59
|
+
id3v2_tag = id3v2_tag_for_version(major_version)
|
60
|
+
|
61
|
+
id3v2_tag.new(minor_version, flags, tag_size, StringIO.new(tag_data), options)
|
62
|
+
end
|
63
|
+
|
64
|
+
# builds an empty id3tag for the given version
|
65
|
+
#
|
66
|
+
# @param version [Integer] the id3tag major version 2,3 or 4
|
67
|
+
# @param options [Options::Options] the options to use
|
68
|
+
#
|
69
|
+
# @return [Id3v22Tag, Id3v23Tag, Id3v24Tag] the Id3v2 tag object for the requested version
|
70
|
+
def self.build_for_version(version, options)
|
71
|
+
case version
|
72
|
+
when 2
|
73
|
+
Id3v22Tag.build_empty(options)
|
74
|
+
when 3
|
75
|
+
Id3v23Tag.build_empty(options)
|
76
|
+
when 4
|
77
|
+
Id3v24Tag.build_empty(options)
|
78
|
+
else
|
79
|
+
raise Errors::Id3TagError, "Id3v2.#{version} is not supported."
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Constructor
|
84
|
+
#
|
85
|
+
# @param header_size [Integer] number of bytes for the Header, usually 6 for v2 and 10 for v3 and v4
|
86
|
+
# @param major_version [Integer] the major version, in v2.[major].[minor]
|
87
|
+
# @param minor_version [Integer] the minor version, in v2.[major].[minor]
|
88
|
+
# @param flags [Header::Id3v2Flags] the 2 Byte header flags wrapped in the entity
|
89
|
+
# @param tag_size [Integer] number of bytes the excluding header/footer of the tag
|
90
|
+
# @param frames [Array<Frames::Id3TagFrame>] array of frames of the id3tag
|
91
|
+
# @param extended_header [Header::Id3v23ExtendedHeader, Header::Id3v24ExtendedHeader, nil] the extended header of
|
92
|
+
# present or nil otherwise
|
93
|
+
def initialize(header_size, major_version, minor_version, flags, tag_size, frames, extended_header)
|
94
|
+
@header_size = header_size
|
95
|
+
@major_version = major_version
|
96
|
+
@minor_version = minor_version
|
97
|
+
@flags = flags
|
98
|
+
@tag_size = tag_size
|
99
|
+
@extended_header = extended_header
|
100
|
+
@frames = frames
|
101
|
+
end
|
102
|
+
|
103
|
+
# gets the version of this id3tag entity
|
104
|
+
#
|
105
|
+
# @return [String] returns the version in the form 2.x.y
|
106
|
+
def version
|
107
|
+
"2.#{@major_version}.#{@minor_version}"
|
108
|
+
end
|
109
|
+
|
110
|
+
# returns the number of bytes of the total tag, including header and footer
|
111
|
+
#
|
112
|
+
# @return [Integer] the total tag size in bytes
|
113
|
+
def total_tag_size
|
114
|
+
@header_size + @tag_size + (@flags&.footer? ? 10 : 0)
|
115
|
+
end
|
116
|
+
|
117
|
+
# selects all frames with the given frame id
|
118
|
+
#
|
119
|
+
# @return [Array<Frames::Id3v2Frame>] an array of Id3v2Frames such as CustomFrame, AlbumFrame etc.
|
120
|
+
def frames(frame_id)
|
121
|
+
@frames.select { |f| f.frame_id == frame_id }
|
122
|
+
end
|
123
|
+
|
124
|
+
# adds a new frame. There will be no validity checks, even invalid frames can be added, essentially rendering
|
125
|
+
# the Id3 tag broken
|
126
|
+
#
|
127
|
+
# @param frame [Frames::Id3v2Frame] the frame to add
|
128
|
+
def add_frame(frame)
|
129
|
+
raise Errors::Id3TagError, 'The given frame is no Id3v2Frame.' unless frame.is_a?(Frames::Id3v2Frame)
|
130
|
+
|
131
|
+
@frames << frame
|
132
|
+
end
|
133
|
+
|
134
|
+
# returns the number of frames for this tag
|
135
|
+
#
|
136
|
+
# @return [Integer] number of frames
|
137
|
+
def number_of_frames
|
138
|
+
@frames.length
|
139
|
+
end
|
140
|
+
|
141
|
+
# determined if the tag is unsynchronized
|
142
|
+
#
|
143
|
+
# @return [Boolean] true if unsynchronized, else false
|
144
|
+
def unsynchronized?
|
145
|
+
@flags.unsynchronized?
|
146
|
+
end
|
147
|
+
|
148
|
+
# sets the tag synchronized or unsynchronized. Should be false, only required for very old player
|
149
|
+
#
|
150
|
+
# @param enabled [Boolean] sets unsynchronized enabled or disabled
|
151
|
+
def unsynchronized=(enabled = true)
|
152
|
+
@flags.unsynchronized = enabled
|
153
|
+
end
|
154
|
+
|
155
|
+
# adds the size tag if not present. If v2.4, the option ignore_v24_frame_error must be true
|
156
|
+
#
|
157
|
+
# @param audio_size [Integer] the audio size bytes
|
158
|
+
def add_size_tag_if_not_present(audio_size)
|
159
|
+
return nil unless @options.add_size_frame
|
160
|
+
|
161
|
+
return nil if @major_version == 4 && !@options.ignore_v24_frame_error
|
162
|
+
|
163
|
+
size_frame = size
|
164
|
+
return nil unless size_frame.nil?
|
165
|
+
|
166
|
+
self.size = audio_size
|
167
|
+
end
|
168
|
+
|
169
|
+
# dumps the tag to a byte string. This dump already takes unsynchronization, padding and all other options
|
170
|
+
# into effect
|
171
|
+
#
|
172
|
+
# @return [String] tag dump as a String. tag.bytes represents the byte array
|
173
|
+
def to_bytes
|
174
|
+
# add up frame size and unsyc if necessary
|
175
|
+
frames_payload = ''
|
176
|
+
@frames.each do |frame|
|
177
|
+
frames_payload += frame.to_bytes
|
178
|
+
end
|
179
|
+
|
180
|
+
frames_payload += "\x00" * @options.padding_bytes if @options.padding_bytes.positive?
|
181
|
+
frames_payload = Util::SyncUtil.add_synchronization(frames_payload) if unsynchronized?
|
182
|
+
frame_size = frames_payload.size
|
183
|
+
|
184
|
+
res = 'ID3'
|
185
|
+
res += @major_version.chr
|
186
|
+
res += @minor_version.chr
|
187
|
+
res += @flags.to_bytes
|
188
|
+
res += Util::MathUtil.from_32_synchsafe_integer(frame_size)
|
189
|
+
res += frames_payload
|
190
|
+
|
191
|
+
if @flags&.footer?
|
192
|
+
res = '3DI'
|
193
|
+
res += @major_version.chr
|
194
|
+
res += @minor_version.chr
|
195
|
+
res += @flags.to_bytes
|
196
|
+
res += Util::MathUtil.from_32_synchsafe_integer(frame_size)
|
197
|
+
end
|
198
|
+
|
199
|
+
res
|
200
|
+
end
|
201
|
+
|
202
|
+
# creates an id3v2 tag for the specific version
|
203
|
+
#
|
204
|
+
# @param major_version [Integer] the major version, 2,3 or 4
|
205
|
+
#
|
206
|
+
# @return [Class<Id3v22Tag>, Class<Id3v23Tag>, Class<Id3v24Tag>] the correct id3v2 tag object
|
207
|
+
def self.id3v2_tag_for_version(major_version)
|
208
|
+
case major_version
|
209
|
+
when 2
|
210
|
+
Id3v22Tag
|
211
|
+
when 3
|
212
|
+
Id3v23Tag
|
213
|
+
when 4
|
214
|
+
Id3v24Tag
|
215
|
+
else
|
216
|
+
raise Errors::Id3TagError, "Unsupported version: 2.#{major_version}"
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# parses the frames for the given id3v2 tag version
|
221
|
+
#
|
222
|
+
# @param file [StringIO, IO, File] the file or StringIO stream the bytes can be fetched from
|
223
|
+
# @param version [Integer]
|
224
|
+
#
|
225
|
+
# @return [Array<Frames::Id3v2Frame>] list of all parsed frames
|
226
|
+
def parse_frames(file, version = 3)
|
227
|
+
generator = Frames::Id3v2FrameFactory.new(file, version, @options)
|
228
|
+
|
229
|
+
result = []
|
230
|
+
frame = generator.next_frame
|
231
|
+
until frame.nil?
|
232
|
+
result << frame
|
233
|
+
frame = generator.next_frame
|
234
|
+
end
|
235
|
+
|
236
|
+
result
|
237
|
+
end
|
238
|
+
|
239
|
+
private_class_method :id3v2_tag_for_version
|
240
|
+
end
|
241
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Id3Taginator
|
4
|
+
module Options
|
5
|
+
class Options
|
6
|
+
|
7
|
+
attr_accessor :default_encode_dest, :default_decode_dest, :padding_bytes, :ignore_v23_frame_error,
|
8
|
+
:ignore_v24_frame_error, :add_size_frame
|
9
|
+
|
10
|
+
# options that are passed through and applied during read and write
|
11
|
+
#
|
12
|
+
# @param default_encode_dest [Encoding] destination encoding is the default encoding that is used for the id3tag
|
13
|
+
# @param default_decode_dest [Encoding] destination decoding is the default encoding that is used when the id3tag
|
14
|
+
# encoding is decoded
|
15
|
+
# @param padding_bytes [Integer] the default padding to add at the end of the tag
|
16
|
+
# @param ignore_v23_frame_error [Boolean] if true, does not throw an error if a frame is added that is not
|
17
|
+
# supported for 2.3 e.g. Sort Order Frames
|
18
|
+
# @param ignore_v24_frame_error [Boolean] if true, does not throw an error if a frame is added that is not
|
19
|
+
# supported for 2.4 e.g. Size Frame
|
20
|
+
# @param add_size_frame [Boolean] if true, the size frame TSIZ will be added automatically to v2.3. Will be added
|
21
|
+
# to v2.4 if ignore_v24_frame_error is true
|
22
|
+
def initialize(default_encode_dest = Encoding::UTF_16, default_decode_dest = Encoding::UTF_8, padding_bytes = 25,
|
23
|
+
ignore_v23_frame_error = true, ignore_v24_frame_error = true, add_size_frame = false)
|
24
|
+
@default_encode_dest = default_encode_dest
|
25
|
+
@default_decode_dest = default_decode_dest
|
26
|
+
@padding_bytes = padding_bytes
|
27
|
+
@ignore_v23_frame_error = ignore_v23_frame_error
|
28
|
+
@ignore_v24_frame_error = ignore_v24_frame_error
|
29
|
+
@add_size_frame = add_size_frame
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Id3Taginator
|
4
|
+
module Util
|
5
|
+
module CompressUtil
|
6
|
+
# compresses the passed data via Zlib
|
7
|
+
#
|
8
|
+
# @param data [String] the data to compress
|
9
|
+
#
|
10
|
+
# @return [String] compressed data
|
11
|
+
def self.decompress_data(data)
|
12
|
+
Zlib::Inflate.inflate(data)
|
13
|
+
end
|
14
|
+
|
15
|
+
# decompresses the passed data via Zlib
|
16
|
+
#
|
17
|
+
# @param data [String] the data to decompress, this must be compressed by Zlib before
|
18
|
+
#
|
19
|
+
# @return [String] decompressed data
|
20
|
+
def self.compress_data(data)
|
21
|
+
Zlib::Deflate.deflate(data)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Id3Taginator
|
4
|
+
module Util
|
5
|
+
module MathUtil
|
6
|
+
# converts a 4 byte synchsafe array to an integer
|
7
|
+
#
|
8
|
+
# @param bytes [Array<Integer>, nil] 4 byte array to convert
|
9
|
+
#
|
10
|
+
# @return [Integer] the 32 bit synchsafe integer
|
11
|
+
def self.to_32_synchsafe_integer(bytes)
|
12
|
+
raise ArgumentError, 'Input can\'t be nil.' if bytes.nil?
|
13
|
+
|
14
|
+
size = 0
|
15
|
+
bytes&.each_with_index do |byte, index|
|
16
|
+
size += 128**(4 - 1 - index) * (byte & 0x7f)
|
17
|
+
end
|
18
|
+
|
19
|
+
size
|
20
|
+
end
|
21
|
+
|
22
|
+
# converts an integer to a 4 byte synchsafe array
|
23
|
+
#
|
24
|
+
# @param num [Integer] given integer to convert
|
25
|
+
#
|
26
|
+
# @return [String] 4 byte String representing the 4 byte synchsafe integer (str.bytes)
|
27
|
+
def self.from_32_synchsafe_integer(num)
|
28
|
+
bytes = Array.new(4, 0)
|
29
|
+
bytes.each_with_index do |_, index|
|
30
|
+
# noinspection RubyMismatchedParameterType
|
31
|
+
bytes[(0 - index).abs], num = num.divmod(128**(4 - 1 - index))
|
32
|
+
end
|
33
|
+
|
34
|
+
bytes.map(&:chr).join
|
35
|
+
end
|
36
|
+
|
37
|
+
# converts a given byte array to the integer representation
|
38
|
+
#
|
39
|
+
# @param bytes [Array<Integer>, nil] byte array to convert
|
40
|
+
#
|
41
|
+
# @return [Integer] the integer
|
42
|
+
# noinspection RubyNilAnalysis
|
43
|
+
def self.to_number(bytes)
|
44
|
+
raise ArgumentError, 'Input can\'t be nil.' if bytes.nil?
|
45
|
+
|
46
|
+
size = 0
|
47
|
+
num_bytes = bytes.length
|
48
|
+
bytes.each_with_index do |byte, index|
|
49
|
+
size += 256**(num_bytes - 1 - index) * (byte & 0xff)
|
50
|
+
end
|
51
|
+
|
52
|
+
size
|
53
|
+
end
|
54
|
+
|
55
|
+
# converts the given number to a byte array, optionally it can be padded using the given char
|
56
|
+
# e.g. given 5 and padding 6 will be 00 00 05 and converted to a byte array string
|
57
|
+
#
|
58
|
+
# @param integer [Integer] the integer to convert
|
59
|
+
# @param padding_to [Integer] pad with char if the given number in hex form has less chars than padding_to
|
60
|
+
# @param char [String] the character to pad
|
61
|
+
#
|
62
|
+
# @return [String] the returning byte array in String representation
|
63
|
+
def self.from_number(integer, padding_to = 0, char = '0')
|
64
|
+
integer.to_s(16).rjust(padding_to, char).scan(/../).map { |x| x.hex.chr }.join
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|