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,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Id3Taginator
|
|
4
|
+
module Frames
|
|
5
|
+
module Count
|
|
6
|
+
class PlayCounterFrame < Id3v2Frame
|
|
7
|
+
include HasId
|
|
8
|
+
|
|
9
|
+
frame_info :CNT, :PCNT, :PCNT
|
|
10
|
+
|
|
11
|
+
attr_accessor :counter
|
|
12
|
+
|
|
13
|
+
# builds the play counter frame
|
|
14
|
+
#
|
|
15
|
+
# @param counter [Integer] the counter, default a 32 bit integer, but can be higher too
|
|
16
|
+
# @param options [Options::Options] options to use
|
|
17
|
+
# @param id3_version [Integer] the id3 version to build the frame for
|
|
18
|
+
#
|
|
19
|
+
# @return [Id3v2Frame] the resulting id3v2 frame
|
|
20
|
+
def self.build_frame(counter, options = nil, id3_version: 3)
|
|
21
|
+
supported?('PCNT', id3_version, options)
|
|
22
|
+
|
|
23
|
+
argument_not_nil(counter, 'counter')
|
|
24
|
+
|
|
25
|
+
frame = new(frame_id(id3_version, options), 0, build_id3_flags(id3_version), '')
|
|
26
|
+
frame.counter = counter
|
|
27
|
+
frame
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def process_content(content)
|
|
31
|
+
@counter = Util::MathUtil.to_number(content.bytes)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def content_to_bytes
|
|
35
|
+
Util::MathUtil.from_number(@counter, 8, '0')
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Id3Taginator
|
|
4
|
+
module Frames
|
|
5
|
+
module Count
|
|
6
|
+
class PopularityFrame < Id3v2Frame
|
|
7
|
+
include HasId
|
|
8
|
+
|
|
9
|
+
frame_info :POP, :POPM, :POPM
|
|
10
|
+
|
|
11
|
+
attr_accessor :email, :rating, :counter
|
|
12
|
+
|
|
13
|
+
# builds the popularimeter frame
|
|
14
|
+
#
|
|
15
|
+
# @param email [String] email of the user
|
|
16
|
+
# @param rating [Integer] the rating between 0 and 255
|
|
17
|
+
# @param counter [Integer] the counter, default 32 bit integer, but can be higher too
|
|
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(email, rating, counter, options = nil, id3_version: 3)
|
|
23
|
+
supported?('POPM', id3_version, options)
|
|
24
|
+
|
|
25
|
+
argument_not_nil(email, 'email')
|
|
26
|
+
argument_not_nil(counter, 'counter')
|
|
27
|
+
|
|
28
|
+
rating ||= 0
|
|
29
|
+
argument_between_num(rating, 'rating', 0, 255)
|
|
30
|
+
|
|
31
|
+
frame = new(frame_id(id3_version, options), 0, build_id3_flags(id3_version), '')
|
|
32
|
+
frame.email = email
|
|
33
|
+
frame.rating = rating
|
|
34
|
+
frame.counter = counter
|
|
35
|
+
frame
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def process_content(content)
|
|
39
|
+
stream = StringIO.new(content)
|
|
40
|
+
@email = read_stream_until(stream, ZERO)
|
|
41
|
+
@rating = stream.readbyte
|
|
42
|
+
@counter = Util::MathUtil.to_number(stream.read&.bytes)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def content_to_bytes
|
|
46
|
+
counter = Util::MathUtil.from_number(@counter, 8, '0')
|
|
47
|
+
merge(@email, "\x00", @rating.chr, counter)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Id3Taginator
|
|
4
|
+
module Frames
|
|
5
|
+
module CountFrames
|
|
6
|
+
include Frames::Frameable
|
|
7
|
+
|
|
8
|
+
# extracts the play count (PCNT/CNT)
|
|
9
|
+
#
|
|
10
|
+
# @return [Integer, nil] returns counter
|
|
11
|
+
def play_counter
|
|
12
|
+
find_frame(Count::PlayCounterFrame.frame_id(@major_version, @options))&.counter
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# sets a counter (PCNT/CNT)
|
|
16
|
+
#
|
|
17
|
+
# @param counter [Integer] the counter to set
|
|
18
|
+
def play_counter=(counter)
|
|
19
|
+
set_frame_fields(Count::PlayCounterFrame, [:@counter], counter)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# removes the play counter frame
|
|
23
|
+
def remove_play_counter
|
|
24
|
+
@frames.delete_if { |f| f.frame_id == Count::PlayCounterFrame.frame_id(@major_version, @options) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# extracts the popularity (POPM/POP)
|
|
28
|
+
#
|
|
29
|
+
# @return [Frames::Count::Entities::Popularimeter, nil] returns popularity
|
|
30
|
+
def popularity
|
|
31
|
+
frame = find_frame(Count::PopularityFrame.frame_id(@major_version, @options))
|
|
32
|
+
return nil if frame.nil?
|
|
33
|
+
|
|
34
|
+
Count::Entities::Popularimeter.new(frame.email, frame.rating, frame.counter)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# sets a counter (POPM/POP)
|
|
38
|
+
#
|
|
39
|
+
# @param popularity [Frames::Count::Entities::Popularimeter] the popularity to set
|
|
40
|
+
def popularity=(popularity)
|
|
41
|
+
set_frame_fields(Count::PopularityFrame, %i[@email @rating @counter],
|
|
42
|
+
popularity.email, popularity.rating, popularity.counter)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# removes the popularity frame
|
|
46
|
+
def remove_popularity
|
|
47
|
+
@frames.delete_if { |f| f.frame_id == Count::PopularityFrame.frame_id(@major_version, @options) }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Id3Taginator
|
|
4
|
+
module Frames
|
|
5
|
+
class CustomFrame < Id3v2Frame
|
|
6
|
+
|
|
7
|
+
attr_accessor :content
|
|
8
|
+
|
|
9
|
+
# builds a custom frame
|
|
10
|
+
#
|
|
11
|
+
# @param content [String] the content
|
|
12
|
+
# @param options [Options::Options] options to use
|
|
13
|
+
# @param id3_version [Integer] the id3 version to build the frame for
|
|
14
|
+
#
|
|
15
|
+
# @return [Id3v2Frame] the resulting id3v2 frame
|
|
16
|
+
def self.build_frame(content, _options = nil, id3_version: 3)
|
|
17
|
+
argument_not_nil(content, 'content')
|
|
18
|
+
|
|
19
|
+
frame = new(frame_id, 0, build_id3_flags(id3_version), '')
|
|
20
|
+
frame.content = content
|
|
21
|
+
frame
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# takes the content as it is, no encoding or other modifications are performed
|
|
25
|
+
#
|
|
26
|
+
# @param content [String] the content
|
|
27
|
+
def process_content(content)
|
|
28
|
+
@content = content
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# returns the content as it is, no encoding or other modifications are performed
|
|
32
|
+
#
|
|
33
|
+
# @return [String] the content
|
|
34
|
+
def content_to_bytes
|
|
35
|
+
@content
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Id3Taginator
|
|
4
|
+
module Frames
|
|
5
|
+
module CustomFrames
|
|
6
|
+
include Frames::Frameable
|
|
7
|
+
|
|
8
|
+
# extracts a custom frame for the given id
|
|
9
|
+
#
|
|
10
|
+
# @param frame_id [Symbol] the frame id
|
|
11
|
+
#
|
|
12
|
+
# @return [String, nil] returns the frame's content data bytes represented as a String (str.bytes)
|
|
13
|
+
def custom_frame(frame_id)
|
|
14
|
+
find_frame(frame_id)&.content
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# adds a custom frame
|
|
18
|
+
# Multiple ones can be added, the selector must be given to determine if a frame should be updated or added
|
|
19
|
+
#
|
|
20
|
+
# @param frame_id [Symbol] the frame id
|
|
21
|
+
# @param content [String] the frame content's data bytes represented as a String (str.bytes)
|
|
22
|
+
# @param selector_lambda [Proc] the lambda to find matching frames to update
|
|
23
|
+
def add_custom_frame(frame_id, content, selector_lambda = nil)
|
|
24
|
+
if selector_lambda.nil?
|
|
25
|
+
existing_frame = find_frame(frame_id)
|
|
26
|
+
else
|
|
27
|
+
existing_frames = find_frames(frame_id)
|
|
28
|
+
existing_frame = existing_frames&.find { |f| selector_lambda.call(f) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
unless existing_frame.nil?
|
|
32
|
+
existing_frame.content = content
|
|
33
|
+
return
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
new_frame = CustomFrame.build_frame(content, @options, id3_version: @major_version)
|
|
37
|
+
new_frame.options = @options
|
|
38
|
+
@frames << new_frame
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# removes the frame with the given id or a specific one with the given selector
|
|
42
|
+
#
|
|
43
|
+
# @param frame_id [String] the frame id to remove
|
|
44
|
+
# @param selector_lambda [Proc] the lambda to find matching frames to update
|
|
45
|
+
def remove_custom_frame(frame_id, selector_lambda = nil)
|
|
46
|
+
if selector_lambda.nil?
|
|
47
|
+
@frames.delete_if { |f| f.frame_id == frame_id }
|
|
48
|
+
else
|
|
49
|
+
@frames.delete_if { |f| f.frame_id == frame_id && selector_lambda.call(f) }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Id3Taginator
|
|
4
|
+
module Frames
|
|
5
|
+
module Encryption
|
|
6
|
+
class AudioEncryptionFrame < Id3v2Frame
|
|
7
|
+
include HasId
|
|
8
|
+
|
|
9
|
+
frame_info :CRA, :AENC, :AENC
|
|
10
|
+
|
|
11
|
+
attr_accessor :owner_id, :preview_start, :preview_length, :encryption_info
|
|
12
|
+
|
|
13
|
+
# builds the audio encryption frame
|
|
14
|
+
#
|
|
15
|
+
# @param owner_id [String] the owner id
|
|
16
|
+
# @param preview_start [Integer] the start frame of the preview
|
|
17
|
+
# @param preview_length [Integer] the preview length in frames
|
|
18
|
+
# @param encryption_info [String] encryption info bytes represented as a String (str.bytes)
|
|
19
|
+
# @param options [Options::Options] options to use
|
|
20
|
+
# @param id3_version [Integer] the id3 version to build the frame for
|
|
21
|
+
#
|
|
22
|
+
# @return [Id3v2Frame] the resulting id3v2 frame
|
|
23
|
+
def self.build_frame(owner_id, preview_start, preview_length, encryption_info, options = nil, id3_version: 3)
|
|
24
|
+
supported?('AENC', id3_version, options)
|
|
25
|
+
|
|
26
|
+
argument_not_nil(owner_id, 'owner_id')
|
|
27
|
+
argument_not_nil(preview_start, 'preview_start')
|
|
28
|
+
argument_not_nil(preview_length, 'preview_length')
|
|
29
|
+
argument_not_nil(encryption_info, 'encryption_info')
|
|
30
|
+
|
|
31
|
+
frame = new(frame_id(id3_version, options), 0, build_id3_flags(id3_version), '')
|
|
32
|
+
frame.owner_id = owner_id
|
|
33
|
+
frame.preview_start = preview_start
|
|
34
|
+
frame.preview_length = preview_length
|
|
35
|
+
frame.encryption_info = encryption_info
|
|
36
|
+
frame
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def process_content(content)
|
|
40
|
+
stream = StringIO.new(content)
|
|
41
|
+
@owner_id = read_stream_until(stream, ZERO)
|
|
42
|
+
@preview_start = Util::MathUtil.to_number(stream.read(2)&.bytes)
|
|
43
|
+
@preview_length = Util::MathUtil.to_number(stream.read(2)&.bytes)
|
|
44
|
+
@encryption_info = stream.read
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def content_to_bytes
|
|
48
|
+
merge(@owner_id, "\x00", Util::MathUtil.from_number(@preview_start, 4),
|
|
49
|
+
Util::MathUtil.from_number(@preview_length, 4), @encryption_info)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Id3Taginator
|
|
4
|
+
module Frames
|
|
5
|
+
module Encryption
|
|
6
|
+
class EncryptionMethodFrame < Id3v2Frame
|
|
7
|
+
include HasId
|
|
8
|
+
|
|
9
|
+
frame_info nil, :ENCR, :ENCR
|
|
10
|
+
|
|
11
|
+
attr_accessor :owner_id, :method_symbol, :encryption_data
|
|
12
|
+
|
|
13
|
+
# builds the encryption method frame
|
|
14
|
+
#
|
|
15
|
+
# @param owner_id [String] the owner id
|
|
16
|
+
# @param method_symbol [Integer] method symbol between 0 and 255
|
|
17
|
+
# @param encryption_data [String] encryption data bytes represented as a String (str.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(owner_id, method_symbol, encryption_data, options = nil, id3_version: 3)
|
|
23
|
+
supported?('ENCR', id3_version, options)
|
|
24
|
+
|
|
25
|
+
argument_not_nil(owner_id, 'owner_id')
|
|
26
|
+
argument_between_num(method_symbol, 'method_symbol', 0, 255)
|
|
27
|
+
argument_not_nil(encryption_data, 'encryption_data')
|
|
28
|
+
|
|
29
|
+
frame = new(frame_id(id3_version, options), 0, build_id3_flags(id3_version), '')
|
|
30
|
+
frame.owner_id = owner_id
|
|
31
|
+
frame.method_symbol = method_symbol
|
|
32
|
+
frame.encryption_data = encryption_data
|
|
33
|
+
frame
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def process_content(content)
|
|
37
|
+
stream = StringIO.new(content)
|
|
38
|
+
@owner_id = read_stream_until(stream, ZERO)
|
|
39
|
+
@method_symbol = stream.readbyte
|
|
40
|
+
@encryption_data = stream.read
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def content_to_bytes
|
|
44
|
+
merge(@owner_id, "\x00", @method_symbol.chr, @encryption_data)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Id3Taginator
|
|
4
|
+
module Frames
|
|
5
|
+
module Encryption
|
|
6
|
+
module Entities
|
|
7
|
+
class AudioEncryption
|
|
8
|
+
include Extensions::Comparable
|
|
9
|
+
|
|
10
|
+
attr_accessor :owner_id, :preview_start, :preview_length, :encryption_info
|
|
11
|
+
|
|
12
|
+
# constructor
|
|
13
|
+
#
|
|
14
|
+
# @param owner_id [String] the owner id
|
|
15
|
+
# @param preview_start [Integer] the start frame of the preview
|
|
16
|
+
# @param preview_length [Integer] the preview length in frames
|
|
17
|
+
# @param encryption_info [String] encryption info represented as a String
|
|
18
|
+
def initialize(owner_id, preview_start, preview_length, encryption_info)
|
|
19
|
+
@owner_id = owner_id
|
|
20
|
+
@preview_start = preview_start
|
|
21
|
+
@preview_length = preview_length
|
|
22
|
+
@encryption_info = encryption_info
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Id3Taginator
|
|
4
|
+
module Frames
|
|
5
|
+
module Encryption
|
|
6
|
+
module Entities
|
|
7
|
+
class EncryptionMethod
|
|
8
|
+
include Extensions::Comparable
|
|
9
|
+
|
|
10
|
+
attr_accessor :owner_id, :method_symbol, :encryption_data
|
|
11
|
+
|
|
12
|
+
# constructor
|
|
13
|
+
#
|
|
14
|
+
# @param owner_id [String] the owner id
|
|
15
|
+
# @param method_symbol [Integer] method symbol between 0 and 255
|
|
16
|
+
# @param encryption_data [String] encryption data represented as a String
|
|
17
|
+
def initialize(owner_id, method_symbol, encryption_data)
|
|
18
|
+
@owner_id = owner_id
|
|
19
|
+
@method_symbol = method_symbol
|
|
20
|
+
@encryption_data = encryption_data
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Id3Taginator
|
|
4
|
+
module Frames
|
|
5
|
+
module EncryptionFrames
|
|
6
|
+
include Frames::Frameable
|
|
7
|
+
|
|
8
|
+
# extracts the audio encryptions (AENC/CRA)
|
|
9
|
+
#
|
|
10
|
+
# @return [Array<Frames::Encryption::Entities::AudioEncryption>] returns the Audio Encryptions
|
|
11
|
+
def audio_encryptions
|
|
12
|
+
frame = find_frames(Encryption::AudioEncryptionFrame.frame_id(@major_version, @options))
|
|
13
|
+
return [] if frame.nil? || frame.empty?
|
|
14
|
+
|
|
15
|
+
frame.map do |f|
|
|
16
|
+
Encryption::Entities::AudioEncryption.new(f.owner_id, f.preview_start, f.preview_length, f.encryption_info)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# adds an audio encryption. (AENC/CRA)
|
|
21
|
+
# Multiple ones can be added, as long as they have different owner_ids
|
|
22
|
+
#
|
|
23
|
+
# @param enc [Frames::Encryption::Entities::AudioEncryption] the audio encryption
|
|
24
|
+
def audio_encryptions=(enc)
|
|
25
|
+
set_frame_fields_by_selector(Encryption::AudioEncryptionFrame,
|
|
26
|
+
%i[@preview_start @preview_length @encryption_info],
|
|
27
|
+
->(f) { f.owner_id == enc.owner_id },
|
|
28
|
+
enc.owner_id, enc.preview_start, enc.preview_length, enc.encryption_info)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
alias add_audio_encryption audio_encryptions=
|
|
32
|
+
|
|
33
|
+
# removes an audio encryption for the specific owner_id (AENC/CRA)
|
|
34
|
+
#
|
|
35
|
+
# @param owner_id [String] the owner_id
|
|
36
|
+
def remove_audio_encryption(owner_id)
|
|
37
|
+
@frames.delete_if do |f|
|
|
38
|
+
f.frame_id == Encryption::AudioEncryptionFrame.frame_id(@major_version, @options) && f.owner_id == owner_id
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# extracts the encryption methods (ENCR)
|
|
43
|
+
#
|
|
44
|
+
# @return [Array<Frames::Encryption::Entities::EncryptionMethod>] returns the Encryption Methods
|
|
45
|
+
def encryption_methods
|
|
46
|
+
frame = find_frames(Encryption::EncryptionMethodFrame.frame_id(@major_version, @options))
|
|
47
|
+
return [] if frame.nil? || frame.empty?
|
|
48
|
+
|
|
49
|
+
frame.map do |f|
|
|
50
|
+
Encryption::Entities::EncryptionMethod.new(f.owner_id, f.method_symbol, f.encryption_data)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# adds an encryption method. (ENCR)
|
|
55
|
+
# Multiple ones can be added, as long as they have different owner_ids
|
|
56
|
+
#
|
|
57
|
+
# @param enc [Frames::Encryption::Entities::AudioEncryption] the audio encryption
|
|
58
|
+
def encryption_methods=(enc)
|
|
59
|
+
set_frame_fields_by_selector(Encryption::EncryptionMethodFrame,
|
|
60
|
+
%i[@owner_id @method_symbol @encryption_data],
|
|
61
|
+
->(f) { f.owner_id == enc.owner_id },
|
|
62
|
+
enc.owner_id, enc.method_symbol, enc.encryption_data)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
alias add_encryption_method encryption_methods=
|
|
66
|
+
|
|
67
|
+
# removes an encryption method for the specific owner_id (ENCR)
|
|
68
|
+
#
|
|
69
|
+
# @param owner_id [String] the owner_id
|
|
70
|
+
def remove_encryption_method(owner_id)
|
|
71
|
+
@frames.delete_if do |f|
|
|
72
|
+
f.frame_id == Encryption::EncryptionMethodFrame.frame_id(@major_version, @options) && f.owner_id == owner_id
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Id3Taginator
|
|
4
|
+
module Frames
|
|
5
|
+
module Frameable
|
|
6
|
+
# raises an error that the given frame is not available for the id3v2 version
|
|
7
|
+
def unsupported_frame(frame_name, version)
|
|
8
|
+
raise Errors::Id3TagError, "#{frame_name} is not supported in Id3v2.#{version}"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# finds the frame with the given frame id, or nil of not present
|
|
12
|
+
#
|
|
13
|
+
# @param frame_id [Symbol] the frame id to search for
|
|
14
|
+
#
|
|
15
|
+
# @return [Id3v2Frame, nil] the found frame or nil, if not present
|
|
16
|
+
def find_frame(frame_id)
|
|
17
|
+
@frames.find { |f| f.frame_id == frame_id }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# finds the frames with the given frame id, or nil of not present
|
|
21
|
+
#
|
|
22
|
+
# @param frame_id [Symbol] the frame id to search for
|
|
23
|
+
#
|
|
24
|
+
# @return [Array<Id3v2Frame>, nil] the found frames or nil, if not present
|
|
25
|
+
def find_frames(frame_id)
|
|
26
|
+
@frames.select { |f| f.frame_id == frame_id }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# sets the given arguments to the appropriate field of the given frame. The field order must be same as the
|
|
30
|
+
# argument order. So e.g. fields a, b, c and arguments a_v, b_v, c_v will lead to a = a_v, b = b_v and c = c_v
|
|
31
|
+
# If the frame already exists, it will be updated. Otherwise it will be created.
|
|
32
|
+
#
|
|
33
|
+
# @param frame [Class<Id3v2Frame>] the frame to set the fields too
|
|
34
|
+
# @param fields [Array<Symbol>] the fields to set, must be in the same order as the arguments
|
|
35
|
+
# @param arguments [Array<Object>] the values to set to the fields
|
|
36
|
+
def set_frame_fields(frame, fields, *arguments)
|
|
37
|
+
existing_frame = find_frame(frame.frame_id(@major_version, @options))
|
|
38
|
+
unless existing_frame.nil?
|
|
39
|
+
fields.each_with_index { |field, index| existing_frame.instance_variable_set(field, arguments[index]) }
|
|
40
|
+
return
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
new_frame = frame.build_frame(*arguments, @options, id3_version: @major_version)
|
|
44
|
+
new_frame.options = @options
|
|
45
|
+
@frames << new_frame
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# sets the given arguments to the appropriate field of the given frame(s). The field order must be same as the
|
|
49
|
+
# argument order. So e.g. fields a, b, c and arguments a_v, b_v, c_v will lead to a = a_v, b = b_v and c = c_v
|
|
50
|
+
# If the frame already exists, it will be updated. Otherwise it will be created.
|
|
51
|
+
# The field will be determined by the given selector_lambda, so e.g.
|
|
52
|
+
# ->(f) { f.descriptor == 'a value' }
|
|
53
|
+
# will be updated, if the frame.descriptor is 'a value'
|
|
54
|
+
#
|
|
55
|
+
# @param frame [Class<Id3v2Frame>] the frame to set the fields too
|
|
56
|
+
# @param fields [Array<Symbol>] the fields to set, must be in the same order as the arguments
|
|
57
|
+
# @param selector_lambda [Proc] the lambda to find matching frames to update
|
|
58
|
+
# @param arguments [Array<Object>] the values to set to the fields
|
|
59
|
+
def set_frame_fields_by_selector(frame, fields, selector_lambda, *arguments)
|
|
60
|
+
existing_frames = find_frames(frame.frame_id(@major_version, @options))
|
|
61
|
+
|
|
62
|
+
existing_frame = existing_frames&.find { |f| selector_lambda.call(f) }
|
|
63
|
+
|
|
64
|
+
unless existing_frame.nil?
|
|
65
|
+
fields.each_with_index { |field, index| existing_frame.instance_variable_set(field, arguments[index]) }
|
|
66
|
+
return
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
new_frame = frame.build_frame(*arguments, @options, id3_version: @major_version)
|
|
70
|
+
new_frame.options = @options
|
|
71
|
+
@frames << new_frame
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Id3Taginator
|
|
4
|
+
module Frames
|
|
5
|
+
module Geo
|
|
6
|
+
module Entities
|
|
7
|
+
class EncapsulatedObject
|
|
8
|
+
include Extensions::Comparable
|
|
9
|
+
|
|
10
|
+
attr_accessor :mime_type, :filename, :descriptor, :object_data
|
|
11
|
+
|
|
12
|
+
# constructor
|
|
13
|
+
#
|
|
14
|
+
# @param mime_type [String] the mime type of the object e.g. image/png
|
|
15
|
+
# @param filename [String] the filename
|
|
16
|
+
# @param descriptor [String] the description
|
|
17
|
+
# @param object_data [String] the object data bytes represented as a String (str.bytes)
|
|
18
|
+
def initialize(mime_type, filename, descriptor, object_data)
|
|
19
|
+
@mime_type = mime_type
|
|
20
|
+
@filename = filename
|
|
21
|
+
@descriptor = descriptor
|
|
22
|
+
@object_data = object_data
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Id3Taginator
|
|
4
|
+
module Frames
|
|
5
|
+
module Geo
|
|
6
|
+
class GeneralEncapsulatedObjectFrame < Id3v2Frame
|
|
7
|
+
include HasId
|
|
8
|
+
|
|
9
|
+
frame_info :GEO, :GEOB, :GEOB
|
|
10
|
+
|
|
11
|
+
attr_accessor :mime_type, :filename, :descriptor, :object_data
|
|
12
|
+
|
|
13
|
+
# builds the general encapsulated object frame
|
|
14
|
+
#
|
|
15
|
+
# @param mime_type [String] the mime type of the object e.g. image/png
|
|
16
|
+
# @param filename [String] the filename
|
|
17
|
+
# @param descriptor [String] the description
|
|
18
|
+
# @param object_data [String] the object data bytes represented as a String (str.bytes)
|
|
19
|
+
# @param options [Options::Options] options to use
|
|
20
|
+
# @param id3_version [Integer] the id3 version to build the frame for
|
|
21
|
+
#
|
|
22
|
+
# @return [Id3v2Frame] the resulting id3v2 frame
|
|
23
|
+
def self.build_frame(mime_type, filename, descriptor, object_data, options = nil, id3_version: 3)
|
|
24
|
+
supported?('GEOB', id3_version, options)
|
|
25
|
+
|
|
26
|
+
mime_type ||= ''
|
|
27
|
+
filename ||= ''
|
|
28
|
+
descriptor ||= ''
|
|
29
|
+
argument_not_nil(object_data, 'object_data')
|
|
30
|
+
|
|
31
|
+
frame = new(frame_id(id3_version, options), 0, build_id3_flags(id3_version), '')
|
|
32
|
+
frame.mime_type = mime_type
|
|
33
|
+
frame.filename = filename
|
|
34
|
+
frame.descriptor = descriptor
|
|
35
|
+
frame.object_data = object_data
|
|
36
|
+
frame
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def process_content(content)
|
|
40
|
+
stream = StringIO.new(content)
|
|
41
|
+
encoding = find_encoding(stream.readbyte)
|
|
42
|
+
|
|
43
|
+
@mime_type = read_stream_until(stream, ZERO)
|
|
44
|
+
encoded_filename = read_stream_until(stream, zero_byte(encoding))
|
|
45
|
+
@filename = decode(encoded_filename, encoding)
|
|
46
|
+
encoded_descriptor = read_stream_until(stream, zero_byte(encoding))
|
|
47
|
+
@descriptor = decode(encoded_descriptor, encoding)
|
|
48
|
+
@object_data = stream.read
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def content_to_bytes
|
|
52
|
+
encoded_filename = encode(@filename, null_terminated: true)
|
|
53
|
+
encoded_descriptor = encode(@descriptor, null_terminated: true)
|
|
54
|
+
merge(default_encoding_destination_byte, @mime_type, ZERO, encoded_filename, encoded_descriptor, @object_data)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Id3Taginator
|
|
4
|
+
module Frames
|
|
5
|
+
module GeoFrames
|
|
6
|
+
include Frames::Frameable
|
|
7
|
+
|
|
8
|
+
# extracts the general encapsulated objects (GEOB/GEO)
|
|
9
|
+
#
|
|
10
|
+
# @return [Array<Frames::Geo::Entities::EncapsulatedObject>] returns the general encapsulated objects
|
|
11
|
+
def encapsulated_objects
|
|
12
|
+
frame = find_frames(Geo::GeneralEncapsulatedObjectFrame.frame_id(@major_version, @options))
|
|
13
|
+
return [] if frame.nil? || frame.empty?
|
|
14
|
+
|
|
15
|
+
frame.map { |f| Geo::Entities::EncapsulatedObject.new(f.mime_type, f.filename, f.descriptor, f.object_data) }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# adds a general encapsulated object (GEOB/GEO)
|
|
19
|
+
#
|
|
20
|
+
# @param object [Frames::Geo::Entities::EncapsulatedObject] the object to add
|
|
21
|
+
def encapsulated_object=(object)
|
|
22
|
+
set_frame_fields_by_selector(Geo::GeneralEncapsulatedObjectFrame, %i[@mime_type @filename @descriptor
|
|
23
|
+
@object_data],
|
|
24
|
+
->(f) { f.descriptor == object.descriptor },
|
|
25
|
+
object.mime_type, object.filename, object.descriptor, object.object_data)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
alias add_encapsulated_object encapsulated_object=
|
|
29
|
+
|
|
30
|
+
# removes an encapsulated object for the specific descriptor
|
|
31
|
+
#
|
|
32
|
+
# @param descriptor [String] the descriptor
|
|
33
|
+
def remove_encapsulated_object(descriptor)
|
|
34
|
+
@frames.delete_if do |f|
|
|
35
|
+
f.frame_id == Geo::GeneralEncapsulatedObjectFrame.frame_id(@major_version, @options) &&
|
|
36
|
+
f.descriptor == descriptor
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|