typo 4.0.0 → 4.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/app/controllers/admin/comments_controller.rb +1 -1
- data/app/controllers/admin/content_controller.rb +1 -3
- data/app/controllers/admin/feedback_controller.rb +36 -31
- data/app/controllers/admin/sidebar_controller.rb +13 -2
- data/app/controllers/admin/users_controller.rb +2 -1
- data/app/controllers/articles_controller.rb +10 -19
- data/app/controllers/xml_controller.rb +2 -2
- data/app/helpers/admin/base_helper.rb +7 -3
- data/app/helpers/application_helper.rb +2 -2
- data/app/helpers/articles_helper.rb +5 -4
- data/app/models/article.rb +16 -10
- data/app/models/blog.rb +4 -10
- data/app/models/comment.rb +17 -36
- data/app/models/content.rb +31 -53
- data/app/models/content_state/base.rb +46 -3
- data/app/models/content_state/draft.rb +2 -9
- data/app/models/content_state/ham.rb +31 -0
- data/app/models/content_state/just_marked_as_ham.rb +10 -0
- data/app/models/content_state/just_marked_as_spam.rb +23 -0
- data/app/models/content_state/just_presumed_ham.rb +37 -0
- data/app/models/content_state/just_published.rb +15 -8
- data/app/models/content_state/new.rb +3 -10
- data/app/models/content_state/presumed_ham.rb +27 -0
- data/app/models/content_state/presumed_spam.rb +31 -0
- data/app/models/content_state/publication_pending.rb +7 -9
- data/app/models/content_state/published.rb +10 -9
- data/app/models/content_state/spam.rb +23 -0
- data/app/models/content_state/unclassified.rb +29 -0
- data/app/models/content_state/withdrawn.rb +28 -0
- data/app/models/email_notifier.rb +0 -1
- data/app/models/feedback.rb +151 -0
- data/app/models/trackback.rb +22 -29
- data/app/views/admin/feedback/_item.rhtml +5 -5
- data/app/views/admin/feedback/list.rhtml +13 -11
- data/app/views/admin/general/index.rhtml +8 -4
- data/app/views/admin/users/show.rhtml +7 -1
- data/app/views/articles/read.rhtml +2 -2
- data/bin/typo +7 -6
- data/components/plugins/sidebars/recent_comments_controller.rb +1 -1
- data/config/environment.rb +2 -0
- data/db/migrate/046_fixup_forthcoming_publications.rb +1 -1
- data/db/migrate/048_remove_count_caching.rb +31 -0
- data/db/migrate/049_move_feedback_to_new_state_machine.rb +33 -0
- data/db/schema.mysql-v3.sql +3 -4
- data/db/schema.mysql.sql +3 -4
- data/db/schema.postgresql.sql +3 -4
- data/db/schema.rb +12 -17
- data/db/schema.sqlite.sql +3 -4
- data/db/schema.sqlserver.sql +3 -4
- data/db/schema_version +1 -1
- data/doc/Installer.txt +4 -0
- data/lib/jabber_notify.rb +6 -2
- data/lib/sidebars/plugin.rb +2 -1
- data/lib/tasks/release.rake +5 -4
- data/lib/typo_version.rb +1 -1
- data/public/stylesheets/administration.css +22 -2
- data/test/fixtures/blogs.yml +2 -0
- data/test/fixtures/contents.yml +7 -7
- data/test/functional/admin/users_controller_test.rb +3 -0
- data/test/functional/articles_controller_test.rb +16 -1
- data/test/mocks/test/xmlrpc_mock.rb +5 -4
- data/test/unit/article_test.rb +16 -4
- data/test/unit/comment_test.rb +57 -35
- data/test/unit/content_state/factory_test.rb +7 -6
- data/test/unit/ping_test.rb +14 -0
- data/test/unit/trackback_test.rb +16 -15
- metadata +26 -26
- data/config/database.yml-pgsql +0 -17
- data/config/database.yml.sqlite +0 -14
- data/config/mail.yml +0 -8
- data/config/mongrel.conf +0 -2
- data/db/converters/mt-import.rb +0 -72
- data/db/development_structure.sql +0 -691
- data/installer/rails-installer.rb +0 -527
- data/installer/rails-installer/commands.rb +0 -118
- data/installer/rails-installer/web-servers.rb +0 -110
- data/log/development.log-1 +0 -991
- data/log/development.log-2 +0 -422
- data/log/development.log-3 +0 -429
- data/log/development.log-4 +0 -174
- data/svk-commitP6cVv.tmp +0 -1
- data/vendor/ruby-mp3info/lib/mp3info.rb +0 -720
data/log/development.log-4
DELETED
@@ -1,174 +0,0 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
Processing ArticlesController#index (for 127.0.0.1 at 2006-07-08 00:17:23) [GET]
|
4
|
-
Parameters: {"action"=>"index", "controller"=>"articles"}
|
5
|
-
[4;35;1mBlog Load (0.001571)[0m [0mSELECT * FROM blogs ORDER BY id LIMIT 1[0m
|
6
|
-
[4;36;1mTrigger Load (0.001660)[0m [0;1mSELECT * FROM triggers WHERE (due_at <= '2006-07-08 00:17:23') [0m
|
7
|
-
[4;35;1mSQL (0.005065)[0m [0m SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull
|
8
|
-
FROM pg_attribute a LEFT JOIN pg_attrdef d
|
9
|
-
ON a.attrelid = d.adrelid AND a.attnum = d.adnum
|
10
|
-
WHERE a.attrelid = 'users'::regclass
|
11
|
-
AND a.attnum > 0 AND NOT a.attisdropped
|
12
|
-
ORDER BY a.attnum
|
13
|
-
[0m
|
14
|
-
[4;36;1mSQL (0.001335)[0m [0;1mSELECT count(*) AS count_all FROM users [0m
|
15
|
-
[4;35;1mSQL (0.003723)[0m [0m SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull
|
16
|
-
FROM pg_attribute a LEFT JOIN pg_attrdef d
|
17
|
-
ON a.attrelid = d.adrelid AND a.attnum = d.adnum
|
18
|
-
WHERE a.attrelid = 'blogs'::regclass
|
19
|
-
AND a.attnum > 0 AND NOT a.attisdropped
|
20
|
-
ORDER BY a.attnum
|
21
|
-
[0m
|
22
|
-
[4;36;1mSQL (0.005265)[0m [0;1m SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull
|
23
|
-
FROM pg_attribute a LEFT JOIN pg_attrdef d
|
24
|
-
ON a.attrelid = d.adrelid AND a.attnum = d.adnum
|
25
|
-
WHERE a.attrelid = 'contents'::regclass
|
26
|
-
AND a.attnum > 0 AND NOT a.attisdropped
|
27
|
-
ORDER BY a.attnum
|
28
|
-
[0m
|
29
|
-
[4;35;1mArticle Count (3.383297)[0m [0mSELECT COUNT(DISTINCT contents.id) FROM contents LEFT OUTER JOIN articles_categories ON articles_categories.article_id = contents.id LEFT OUTER JOIN categories ON categories.id = articles_categories.category_id LEFT OUTER JOIN articles_tags ON articles_tags.article_id = contents.id LEFT OUTER JOIN tags ON tags.id = articles_tags.tag_id WHERE (published = 't' AND contents.created_at < '2006-07-08 00:17:23' AND blog_id = 1) AND ( (contents."type" = 'Article' ) ) [0m
|
30
|
-
[4;36;1mSQL (0.004491)[0m [0;1m SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull
|
31
|
-
FROM pg_attribute a LEFT JOIN pg_attrdef d
|
32
|
-
ON a.attrelid = d.adrelid AND a.attnum = d.adnum
|
33
|
-
WHERE a.attrelid = 'categories'::regclass
|
34
|
-
AND a.attnum > 0 AND NOT a.attisdropped
|
35
|
-
ORDER BY a.attnum
|
36
|
-
[0m
|
37
|
-
[4;35;1mSQL (0.005264)[0m [0m SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull
|
38
|
-
FROM pg_attribute a LEFT JOIN pg_attrdef d
|
39
|
-
ON a.attrelid = d.adrelid AND a.attnum = d.adnum
|
40
|
-
WHERE a.attrelid = 'tags'::regclass
|
41
|
-
AND a.attnum > 0 AND NOT a.attisdropped
|
42
|
-
ORDER BY a.attnum
|
43
|
-
[0m
|
44
|
-
[4;36;1mArticle Load IDs For Limited Eager Loading (0.016903)[0m [0;1mSELECT id FROM contents WHERE (published = 't' AND contents.created_at < '2006-07-08 00:17:23' AND blog_id = 1) AND ( (contents."type" = 'Article' ) ) ORDER BY contents.published_at DESC LIMIT 15 OFFSET 0[0m
|
45
|
-
[4;35;1mArticle Load Including Associations (0.150279)[0m [0mSELECT contents."id" AS t0_r0, contents."title" AS t0_r1, contents."author" AS t0_r2, contents."body" AS t0_r3, contents."body_html" AS t0_r4, contents."extended" AS t0_r5, contents."excerpt" AS t0_r6, contents."keywords" AS t0_r7, contents."created_at" AS t0_r8, contents."updated_at" AS t0_r9, contents."extended_html" AS t0_r10, contents."user_id" AS t0_r11, contents."permalink" AS t0_r12, contents."guid" AS t0_r13, contents."text_filter_id" AS t0_r14, contents."whiteboard" AS t0_r15, contents."comments_count" AS t0_r16, contents."trackbacks_count" AS t0_r17, contents."type" AS t0_r18, contents."article_id" AS t0_r19, contents."email" AS t0_r20, contents."url" AS t0_r21, contents."ip" AS t0_r22, contents."blog_name" AS t0_r23, contents."name" AS t0_r24, contents."published" AS t0_r25, contents."allow_pings" AS t0_r26, contents."allow_comments" AS t0_r27, contents."blog_id" AS t0_r28, contents."published_at" AS t0_r29, categories."id" AS t1_r0, categories."name" AS t1_r1, categories."position" AS t1_r2, categories."is_primary" AS t1_r3, categories."permalink" AS t1_r4, tags."id" AS t2_r0, tags."name" AS t2_r1, tags."created_at" AS t2_r2, tags."updated_at" AS t2_r3, tags."display_name" AS t2_r4 FROM contents LEFT OUTER JOIN articles_categories ON articles_categories.article_id = contents.id LEFT OUTER JOIN categories ON categories.id = articles_categories.category_id LEFT OUTER JOIN articles_tags ON articles_tags.article_id = contents.id LEFT OUTER JOIN tags ON tags.id = articles_tags.tag_id WHERE (published = 't' AND contents.created_at < '2006-07-08 00:17:23' AND blog_id = 1) AND ( (contents."type" = 'Article' ) ) AND contents.id IN ('1640', '562', '561', '560', '559', '558', '557', '556', '555', '554', '553', '552', '551', '550', '549') ORDER BY contents.published_at DESC [0m
|
46
|
-
Rendering within ../../themes/scribbish/layouts/default
|
47
|
-
Rendering articles/index
|
48
|
-
[4;36;1mBlog Load (0.002095)[0m [0;1mSELECT * FROM blogs WHERE (blogs.id = 1) LIMIT 1[0m
|
49
|
-
[4;35;1mUser Load (0.002659)[0m [0mSELECT * FROM users WHERE (users.id = 1) LIMIT 1[0m
|
50
|
-
[4;36;1mBlog Load (0.001452)[0m [0;1mSELECT * FROM blogs ORDER BY id LIMIT 1[0m
|
51
|
-
Rendered articles/_article (0.07813)
|
52
|
-
[4;35;1mBlog Load (0.001730)[0m [0mSELECT * FROM blogs WHERE (blogs.id = 1) LIMIT 1[0m
|
53
|
-
[4;36;1mUser Load (0.002122)[0m [0;1mSELECT * FROM users WHERE (users.id = 1) LIMIT 1[0m
|
54
|
-
Rendered articles/_article (0.01851)
|
55
|
-
[4;35;1mBlog Load (0.002361)[0m [0mSELECT * FROM blogs WHERE (blogs.id = 1) LIMIT 1[0m
|
56
|
-
[4;36;1mUser Load (0.001859)[0m [0;1mSELECT * FROM users WHERE (users.id = 1) LIMIT 1[0m
|
57
|
-
Rendered articles/_article (0.01787)
|
58
|
-
[4;35;1mBlog Load (0.002031)[0m [0mSELECT * FROM blogs WHERE (blogs.id = 1) LIMIT 1[0m
|
59
|
-
[4;36;1mUser Load (0.001887)[0m [0;1mSELECT * FROM users WHERE (users.id = 1) LIMIT 1[0m
|
60
|
-
Rendered articles/_article (0.13746)
|
61
|
-
[4;35;1mBlog Load (0.001752)[0m [0mSELECT * FROM blogs WHERE (blogs.id = 1) LIMIT 1[0m
|
62
|
-
[4;36;1mUser Load (0.002257)[0m [0;1mSELECT * FROM users WHERE (users.id = 1) LIMIT 1[0m
|
63
|
-
Rendered articles/_article (0.01825)
|
64
|
-
[4;35;1mBlog Load (0.001933)[0m [0mSELECT * FROM blogs WHERE (blogs.id = 1) LIMIT 1[0m
|
65
|
-
[4;36;1mUser Load (0.155534)[0m [0;1mSELECT * FROM users WHERE (users.id = 1) LIMIT 1[0m
|
66
|
-
Rendered articles/_article (0.26178)
|
67
|
-
[4;35;1mBlog Load (0.001735)[0m [0mSELECT * FROM blogs WHERE (blogs.id = 1) LIMIT 1[0m
|
68
|
-
[4;36;1mUser Load (0.002489)[0m [0;1mSELECT * FROM users WHERE (users.id = 1) LIMIT 1[0m
|
69
|
-
Rendered articles/_article (0.01920)
|
70
|
-
[4;35;1mBlog Load (0.002814)[0m [0mSELECT * FROM blogs WHERE (blogs.id = 1) LIMIT 1[0m
|
71
|
-
[4;36;1mUser Load (0.001888)[0m [0;1mSELECT * FROM users WHERE (users.id = 1) LIMIT 1[0m
|
72
|
-
Rendered articles/_article (0.13118)
|
73
|
-
[4;35;1mBlog Load (0.001763)[0m [0mSELECT * FROM blogs WHERE (blogs.id = 1) LIMIT 1[0m
|
74
|
-
[4;36;1mUser Load (0.001945)[0m [0;1mSELECT * FROM users WHERE (users.id = 1) LIMIT 1[0m
|
75
|
-
Rendered articles/_article (0.01881)
|
76
|
-
[4;35;1mBlog Load (0.002165)[0m [0mSELECT * FROM blogs WHERE (blogs.id = 1) LIMIT 1[0m
|
77
|
-
[4;36;1mUser Load (0.001878)[0m [0;1mSELECT * FROM users WHERE (users.id = 1) LIMIT 1[0m
|
78
|
-
Rendered articles/_article (0.01625)
|
79
|
-
[4;35;1mBlog Load (0.003068)[0m [0mSELECT * FROM blogs WHERE (blogs.id = 1) LIMIT 1[0m
|
80
|
-
[4;36;1mUser Load (0.001868)[0m [0;1mSELECT * FROM users WHERE (users.id = 1) LIMIT 1[0m
|
81
|
-
Rendered articles/_article (0.01773)
|
82
|
-
[4;35;1mBlog Load (0.001745)[0m [0mSELECT * FROM blogs WHERE (blogs.id = 1) LIMIT 1[0m
|
83
|
-
[4;36;1mUser Load (0.002177)[0m [0;1mSELECT * FROM users WHERE (users.id = 1) LIMIT 1[0m
|
84
|
-
Rendered articles/_article (0.01745)
|
85
|
-
[4;35;1mBlog Load (0.001913)[0m [0mSELECT * FROM blogs WHERE (blogs.id = 1) LIMIT 1[0m
|
86
|
-
[4;36;1mUser Load (0.001883)[0m [0;1mSELECT * FROM users WHERE (users.id = 1) LIMIT 1[0m
|
87
|
-
Rendered articles/_article (0.12971)
|
88
|
-
[4;35;1mBlog Load (0.002062)[0m [0mSELECT * FROM blogs WHERE (blogs.id = 1) LIMIT 1[0m
|
89
|
-
[4;36;1mUser Load (0.001928)[0m [0;1mSELECT * FROM users WHERE (users.id = 1) LIMIT 1[0m
|
90
|
-
Rendered articles/_article (0.01989)
|
91
|
-
[4;35;1mBlog Load (0.001737)[0m [0mSELECT * FROM blogs WHERE (blogs.id = 1) LIMIT 1[0m
|
92
|
-
[4;36;1mUser Load (0.001901)[0m [0;1mSELECT * FROM users WHERE (users.id = 1) LIMIT 1[0m
|
93
|
-
Rendered articles/_article (0.01962)
|
94
|
-
Rendered articles/_search (0.00467)
|
95
|
-
Start rendering component ({:controller=>SidebarController, :action=>"display_plugins"}):
|
96
|
-
|
97
|
-
|
98
|
-
Processing SidebarController#display_plugins (for 127.0.0.1 at 2006-07-08 00:17:28)
|
99
|
-
[4;35;1mBlog Load (0.001420)[0m [0mSELECT * FROM blogs ORDER BY id LIMIT 1[0m
|
100
|
-
[4;36;1mTrigger Load (0.001471)[0m [0;1mSELECT * FROM triggers WHERE (due_at <= '2006-07-08 00:17:28') [0m
|
101
|
-
[4;35;1mSidebar Load (0.005570)[0m [0mSELECT * FROM sidebars WHERE (active_position is not null) ORDER BY active_position [0m
|
102
|
-
[4;36;1mSQL (0.005188)[0m [0;1m SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull
|
103
|
-
FROM pg_attribute a LEFT JOIN pg_attrdef d
|
104
|
-
ON a.attrelid = d.adrelid AND a.attnum = d.adnum
|
105
|
-
WHERE a.attrelid = 'sidebars'::regclass
|
106
|
-
AND a.attnum > 0 AND NOT a.attisdropped
|
107
|
-
ORDER BY a.attnum
|
108
|
-
[0m
|
109
|
-
Rendering sidebar/display_plugins
|
110
|
-
Start rendering component ({:controller=>Plugins::Sidebars::TagController, :action=>"index", :layout=>false}):
|
111
|
-
|
112
|
-
|
113
|
-
Processing TagController#index (for 127.0.0.1 at 2006-07-08 00:17:28)
|
114
|
-
[4;35;1mBlog Load (0.001579)[0m [0mSELECT * FROM blogs ORDER BY id LIMIT 1[0m
|
115
|
-
[4;36;1mTrigger Load (0.001495)[0m [0;1mSELECT * FROM triggers WHERE (due_at <= '2006-07-08 00:17:28') [0m
|
116
|
-
[4;35;1mTag Load (0.045230)[0m [0m
|
117
|
-
SELECT tags.id, tags.name, tags.display_name, COUNT(articles_tags.article_id) AS article_counter
|
118
|
-
FROM tags tags LEFT OUTER JOIN articles_tags articles_tags
|
119
|
-
ON articles_tags.tag_id = tags.id
|
120
|
-
LEFT OUTER JOIN contents articles
|
121
|
-
ON articles_tags.article_id = articles.id
|
122
|
-
WHERE articles.published = 't'
|
123
|
-
GROUP BY tags.id, tags.name, tags.display_name
|
124
|
-
ORDER BY article_counter DESC
|
125
|
-
LIMIT 20
|
126
|
-
[0m
|
127
|
-
Rendering plugins/sidebars/tag/content
|
128
|
-
Completed in 0.18452 (5 reqs/sec) | Rendering: 0.12484 (67%) | DB: 0.04830 (26%) | 200 OK [http://localhost/]
|
129
|
-
|
130
|
-
|
131
|
-
End of component rendering
|
132
|
-
BENCHMARK: display_plugins: tag (0.18900)
|
133
|
-
Rendered sidebar/_sidebar (0.18990)
|
134
|
-
Start rendering component ({:controller=>Plugins::Sidebars::ArchivesController, :action=>"index", :layout=>false}):
|
135
|
-
|
136
|
-
|
137
|
-
Processing ArchivesController#index (for 127.0.0.1 at 2006-07-08 00:17:28)
|
138
|
-
[4;36;1mBlog Load (0.001800)[0m [0;1mSELECT * FROM blogs ORDER BY id LIMIT 1[0m
|
139
|
-
[4;35;1mTrigger Load (0.001495)[0m [0mSELECT * FROM triggers WHERE (due_at <= '2006-07-08 00:17:28') [0m
|
140
|
-
[4;36;1mContent Load (0.023723)[0m [0;1mselect count(*) as count, extract(year from published_at)||' '||lpad(extract(month from published_at),2,'0') as date from contents where type='Article' and published = 't' and published_at < '2006-07-08 00:17:28' group by date order by date desc limit '10'[0m
|
141
|
-
[4;35;1mSQL (0.004369)[0m [0m SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull
|
142
|
-
FROM pg_attribute a LEFT JOIN pg_attrdef d
|
143
|
-
ON a.attrelid = d.adrelid AND a.attnum = d.adnum
|
144
|
-
WHERE a.attrelid = 'contents'::regclass
|
145
|
-
AND a.attnum > 0 AND NOT a.attisdropped
|
146
|
-
ORDER BY a.attnum
|
147
|
-
[0m
|
148
|
-
Rendering plugins/sidebars/archives/content
|
149
|
-
Completed in 0.18893 (5 reqs/sec) | Rendering: 0.00836 (4%) | DB: 0.03139 (16%) | 200 OK [http://localhost/]
|
150
|
-
|
151
|
-
|
152
|
-
End of component rendering
|
153
|
-
BENCHMARK: display_plugins: archives (0.19216)
|
154
|
-
Rendered sidebar/_sidebar (0.19309)
|
155
|
-
Start rendering component ({:controller=>Plugins::Sidebars::StaticController, :action=>"index", :layout=>false}):
|
156
|
-
|
157
|
-
|
158
|
-
Processing StaticController#index (for 127.0.0.1 at 2006-07-08 00:17:28)
|
159
|
-
[4;36;1mBlog Load (0.001424)[0m [0;1mSELECT * FROM blogs ORDER BY id LIMIT 1[0m
|
160
|
-
[4;35;1mTrigger Load (0.002276)[0m [0mSELECT * FROM triggers WHERE (due_at <= '2006-07-08 00:17:28') [0m
|
161
|
-
Rendering plugins/sidebars/static/content
|
162
|
-
Completed in 0.13924 (7 reqs/sec) | Rendering: 0.00240 (1%) | DB: 0.00370 (2%) | 200 OK [http://localhost/]
|
163
|
-
|
164
|
-
|
165
|
-
End of component rendering
|
166
|
-
BENCHMARK: display_plugins: static (0.14366)
|
167
|
-
Rendered sidebar/_sidebar (0.14466)
|
168
|
-
BENCHMARK: display_plugins (0.53017)
|
169
|
-
Completed in 0.56401 (1 reqs/sec) | Rendering: 0.53488 (94%) | DB: 0.23028 (40%) | 200 OK [http://localhost/]
|
170
|
-
|
171
|
-
|
172
|
-
End of component rendering
|
173
|
-
[4;36;1mArticle Load (0.011033)[0m [0;1mSELECT * FROM contents WHERE (published = 't' AND created_at > '2006-07-08 00:17:23') AND ( (contents."type" = 'Article' ) ) ORDER BY created_at ASC LIMIT 1[0m
|
174
|
-
Completed in 5.79087 (0 reqs/sec) | Rendering: 1.89160 (32%) | DB: 3.58989 (61%) | 200 OK [http://localhost/]
|
data/svk-commitP6cVv.tmp
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
Tagging 3.99.4
|
@@ -1,720 +0,0 @@
|
|
1
|
-
# $Id: mp3info.rb,v 1.5 2005/04/26 13:41:41 moumar Exp $
|
2
|
-
# = Description
|
3
|
-
#
|
4
|
-
# ruby-mp3info gives you access to low level informations on mp3 files
|
5
|
-
# (bitrate, length, samplerate, etc...). It can read, write, remove id3v1 tag
|
6
|
-
# and read id3v2. It is written in pure ruby.
|
7
|
-
#
|
8
|
-
#
|
9
|
-
# = Download
|
10
|
-
#
|
11
|
-
# get tar.gz at
|
12
|
-
# http://rubyforge.org/projects/ruby-mp3info/
|
13
|
-
#
|
14
|
-
#
|
15
|
-
# = Installation
|
16
|
-
#
|
17
|
-
# $ ruby install.rb config
|
18
|
-
# $ ruby install.rb setup
|
19
|
-
# # ruby install.rb install
|
20
|
-
#
|
21
|
-
# or
|
22
|
-
#
|
23
|
-
# # gem install ruby-mp3info
|
24
|
-
#
|
25
|
-
#
|
26
|
-
# = Example
|
27
|
-
#
|
28
|
-
# require "mp3info"
|
29
|
-
#
|
30
|
-
# mp3info = Mp3Info.new("myfile.mp3")
|
31
|
-
# puts mp3info
|
32
|
-
#
|
33
|
-
#
|
34
|
-
# = Testing
|
35
|
-
#
|
36
|
-
# Test::Unit library is used for tests. see http://testunit.talbott.ws/
|
37
|
-
#
|
38
|
-
# $ ruby test.rb
|
39
|
-
#
|
40
|
-
#
|
41
|
-
# = ToDo
|
42
|
-
#
|
43
|
-
# * adding write support for ID3v2 tags
|
44
|
-
# * adding a test for id3v2
|
45
|
-
# * encoder detection
|
46
|
-
#
|
47
|
-
#
|
48
|
-
# = Changelog
|
49
|
-
#
|
50
|
-
# [0.4 26/04/2005]
|
51
|
-
#
|
52
|
-
# * fixes in vbr mode
|
53
|
-
# * removed extract_info_from_head() function
|
54
|
-
# * now try several times to find a good header frame before giving up
|
55
|
-
# * correct handling of unicode in v2 tags. Require standard "iconv" library if such tags are used
|
56
|
-
# * FIXED if a tag appears more than one time, create an array with every value found for this tag
|
57
|
-
#
|
58
|
-
#
|
59
|
-
# [0.3 04/05/2004]
|
60
|
-
#
|
61
|
-
# * massive changes of most of the code to make it easier to read & hopefully run faster
|
62
|
-
# * ID2TAGS hash is just informative now, no use of it in the code. id3v2 tag fields are read in directly
|
63
|
-
# * added support for id3 v2.2 and v2.4 (0.2.1 only supported v2.3)
|
64
|
-
# * much improved vbr duration guessing
|
65
|
-
# * made Mp3Info#to_s output to be prettier
|
66
|
-
# * moved hastag1? and hastag2? to be class booleans instead of functions (now named hastag1 and hastag2)
|
67
|
-
# * fixed a bug on computing "error_protection" attribute
|
68
|
-
# * new attribute "tag", which is a sort of "universal" tag, regardless of the tag version, 1 or 2, with the same keys as @tag1
|
69
|
-
# * new method hastag?, which test the presence of any tag
|
70
|
-
#
|
71
|
-
#
|
72
|
-
# [0.2.1 04/09/2003]
|
73
|
-
#
|
74
|
-
# * filename attribute added
|
75
|
-
# * mp3 files are opened read-only now [Alan Davies <alan__DOT_davies__AT__thomson.com>]
|
76
|
-
# * Mp3Info#initialize: bugfixes [Alan Davies <alan__DOT_davies__AT__thomson.com>]
|
77
|
-
# * put NULLs in year field in id3v1 tags instead of zeros [Alan Davies <alan__DOT_davies__AT__thomson.com>]
|
78
|
-
# * Mp3Info#gettag1: remove null at end of strings [Alan Davies <alan__DOT_davies__AT__thomson.com>]
|
79
|
-
# * Mp3Info#extract_infos_from_head(): some brackets missed [Alan Davies <alan__DOT_davies__AT__thomson.com>]
|
80
|
-
#
|
81
|
-
#
|
82
|
-
# [0.2 18/08/2003]
|
83
|
-
#
|
84
|
-
# * writing, reading and removing of id3v1 tags
|
85
|
-
# * reading of id3v2 tags
|
86
|
-
# * test suite improved
|
87
|
-
# * to_s method added
|
88
|
-
# * length attribute is a Float now
|
89
|
-
#
|
90
|
-
#
|
91
|
-
# [0.1 17/03/2003]
|
92
|
-
#
|
93
|
-
# * Initial version
|
94
|
-
#
|
95
|
-
#
|
96
|
-
# License:: Ruby
|
97
|
-
# Author:: Guillaume Pierronnet (mailto:moumar_AT__rubyforge_DOT_org)
|
98
|
-
# Website:: http://ruby-mp3info.rubyforge.org/
|
99
|
-
|
100
|
-
# Raised on any kind of error related to ruby-mp3info
|
101
|
-
class Mp3InfoError < StandardError ; end
|
102
|
-
|
103
|
-
class Mp3InfoInternalError < StandardError #:nodoc:
|
104
|
-
end
|
105
|
-
|
106
|
-
class Numeric
|
107
|
-
### returns the selected bit range (b, a) as a number
|
108
|
-
### NOTE: b > a if not, returns 0
|
109
|
-
def bits(b, a)
|
110
|
-
t = 0
|
111
|
-
b.downto(a) { |i| t += t + self[i] }
|
112
|
-
t
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
class Hash
|
117
|
-
### lets you specify hash["key"] as hash.key
|
118
|
-
### this came from CodingInRuby on RubyGarden
|
119
|
-
### http://www.rubygarden.org/ruby?CodingInRuby
|
120
|
-
def method_missing(meth,*args)
|
121
|
-
if /=$/=~(meth=meth.id2name) then
|
122
|
-
self[meth[0...-1]] = (args.length<2 ? args[0] : args)
|
123
|
-
else
|
124
|
-
self[meth]
|
125
|
-
end
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
|
-
class File
|
130
|
-
def get32bits
|
131
|
-
(getc << 24) + (getc << 16) + (getc << 8) + getc
|
132
|
-
end
|
133
|
-
def get_syncsafe
|
134
|
-
(getc << 21) + (getc << 14) + (getc << 7) + getc
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
class Mp3Info
|
139
|
-
|
140
|
-
VERSION = "0.4"
|
141
|
-
|
142
|
-
LAYER = [ nil, 3, 2, 1]
|
143
|
-
BITRATE = [
|
144
|
-
[
|
145
|
-
[32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448],
|
146
|
-
[32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384],
|
147
|
-
[32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320] ],
|
148
|
-
[
|
149
|
-
[32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256],
|
150
|
-
[8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160],
|
151
|
-
[8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160]
|
152
|
-
]
|
153
|
-
]
|
154
|
-
SAMPLERATE = [
|
155
|
-
[ 44100, 48000, 32000 ],
|
156
|
-
[ 22050, 24000, 16000 ]
|
157
|
-
]
|
158
|
-
CHANNEL_MODE = [ "Stereo", "JStereo", "Dual Channel", "Single Channel"]
|
159
|
-
|
160
|
-
GENRES = [
|
161
|
-
"Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk",
|
162
|
-
"Grunge", "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies",
|
163
|
-
"Other", "Pop", "R&B", "Rap", "Reggae", "Rock",
|
164
|
-
"Techno", "Industrial", "Alternative", "Ska", "Death Metal", "Pranks",
|
165
|
-
"Soundtrack", "Euro-Techno", "Ambient", "Trip-Hop", "Vocal", "Jazz+Funk",
|
166
|
-
"Fusion", "Trance", "Classical", "Instrumental", "Acid", "House",
|
167
|
-
"Game", "Sound Clip", "Gospel", "Noise", "AlternRock", "Bass",
|
168
|
-
"Soul", "Punk", "Space", "Meditative", "Instrumental Pop", "Instrumental Rock",
|
169
|
-
"Ethnic", "Gothic", "Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk",
|
170
|
-
"Eurodance", "Dream", "Southern Rock", "Comedy", "Cult", "Gangsta",
|
171
|
-
"Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native American", "Cabaret",
|
172
|
-
"New Wave", "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi",
|
173
|
-
"Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical",
|
174
|
-
"Rock & Roll", "Hard Rock", "Folk", "Folk/Rock", "National Folk", "Swing",
|
175
|
-
"Fast-Fusion", "Bebob", "Latin", "Revival", "Celtic", "Bluegrass", "Avantgarde",
|
176
|
-
"Gothic Rock", "Progressive Rock", "Psychedelic Rock", "Symphonic Rock", "Slow Rock", "Big Band",
|
177
|
-
"Chorus", "Easy Listening", "Acoustic", "Humour", "Speech", "Chanson",
|
178
|
-
"Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass", "Primus",
|
179
|
-
"Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba",
|
180
|
-
"Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", "Freestyle", "Duet",
|
181
|
-
"Punk Rock", "Drum Solo", "A capella", "Euro-House", "Dance Hall",
|
182
|
-
"Goa", "Drum & Bass", "Club House", "Hardcore", "Terror",
|
183
|
-
"Indie", "BritPop", "NegerPunk", "Polsk Punk", "Beat",
|
184
|
-
"Christian Gangsta", "Heavy Metal", "Black Metal", "Crossover", "Contemporary C",
|
185
|
-
"Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", "JPop",
|
186
|
-
"SynthPop" ]
|
187
|
-
|
188
|
-
|
189
|
-
ID2TAGS = {
|
190
|
-
"AENC" => "Audio encryption",
|
191
|
-
"APIC" => "Attached picture",
|
192
|
-
"COMM" => "Comments",
|
193
|
-
"COMR" => "Commercial frame",
|
194
|
-
"ENCR" => "Encryption method registration",
|
195
|
-
"EQUA" => "Equalization",
|
196
|
-
"ETCO" => "Event timing codes",
|
197
|
-
"GEOB" => "General encapsulated object",
|
198
|
-
"GRID" => "Group identification registration",
|
199
|
-
"IPLS" => "Involved people list",
|
200
|
-
"LINK" => "Linked information",
|
201
|
-
"MCDI" => "Music CD identifier",
|
202
|
-
"MLLT" => "MPEG location lookup table",
|
203
|
-
"OWNE" => "Ownership frame",
|
204
|
-
"PRIV" => "Private frame",
|
205
|
-
"PCNT" => "Play counter",
|
206
|
-
"POPM" => "Popularimeter",
|
207
|
-
"POSS" => "Position synchronisation frame",
|
208
|
-
"RBUF" => "Recommended buffer size",
|
209
|
-
"RVAD" => "Relative volume adjustment",
|
210
|
-
"RVRB" => "Reverb",
|
211
|
-
"SYLT" => "Synchronized lyric/text",
|
212
|
-
"SYTC" => "Synchronized tempo codes",
|
213
|
-
"TALB" => "Album/Movie/Show title",
|
214
|
-
"TBPM" => "BPM (beats per minute)",
|
215
|
-
"TCOM" => "Composer",
|
216
|
-
"TCON" => "Content type",
|
217
|
-
"TCOP" => "Copyright message",
|
218
|
-
"TDAT" => "Date",
|
219
|
-
"TDLY" => "Playlist delay",
|
220
|
-
"TENC" => "Encoded by",
|
221
|
-
"TEXT" => "Lyricist/Text writer",
|
222
|
-
"TFLT" => "File type",
|
223
|
-
"TIME" => "Time",
|
224
|
-
"TIT1" => "Content group description",
|
225
|
-
"TIT2" => "Title/songname/content description",
|
226
|
-
"TIT3" => "Subtitle/Description refinement",
|
227
|
-
"TKEY" => "Initial key",
|
228
|
-
"TLAN" => "Language(s)",
|
229
|
-
"TLEN" => "Length",
|
230
|
-
"TMED" => "Media type",
|
231
|
-
"TOAL" => "Original album/movie/show title",
|
232
|
-
"TOFN" => "Original filename",
|
233
|
-
"TOLY" => "Original lyricist(s)/text writer(s)",
|
234
|
-
"TOPE" => "Original artist(s)/performer(s)",
|
235
|
-
"TORY" => "Original release year",
|
236
|
-
"TOWN" => "File owner/licensee",
|
237
|
-
"TPE1" => "Lead performer(s)/Soloist(s)",
|
238
|
-
"TPE2" => "Band/orchestra/accompaniment",
|
239
|
-
"TPE3" => "Conductor/performer refinement",
|
240
|
-
"TPE4" => "Interpreted, remixed, or otherwise modified by",
|
241
|
-
"TPOS" => "Part of a set",
|
242
|
-
"TPUB" => "Publisher",
|
243
|
-
"TRCK" => "Track number/Position in set",
|
244
|
-
"TRDA" => "Recording dates",
|
245
|
-
"TRSN" => "Internet radio station name",
|
246
|
-
"TRSO" => "Internet radio station owner",
|
247
|
-
"TSIZ" => "Size",
|
248
|
-
"TSRC" => "ISRC (international standard recording code)",
|
249
|
-
"TSSE" => "Software/Hardware and settings used for encoding",
|
250
|
-
"TYER" => "Year",
|
251
|
-
"TXXX" => "User defined text information frame",
|
252
|
-
"UFID" => "Unique file identifier",
|
253
|
-
"USER" => "Terms of use",
|
254
|
-
"USLT" => "Unsychronized lyric/text transcription",
|
255
|
-
"WCOM" => "Commercial information",
|
256
|
-
"WCOP" => "Copyright/Legal information",
|
257
|
-
"WOAF" => "Official audio file webpage",
|
258
|
-
"WOAR" => "Official artist/performer webpage",
|
259
|
-
"WOAS" => "Official audio source webpage",
|
260
|
-
"WORS" => "Official internet radio station homepage",
|
261
|
-
"WPAY" => "Payment",
|
262
|
-
"WPUB" => "Publishers official webpage",
|
263
|
-
"WXXX" => "User defined URL link frame"
|
264
|
-
}
|
265
|
-
|
266
|
-
TAGSIZE = 128
|
267
|
-
#MAX_FRAME_COUNT = 6 #number of frame to read for encoder detection
|
268
|
-
|
269
|
-
# mpeg version = 1 or 2
|
270
|
-
attr_reader(:mpeg_version)
|
271
|
-
|
272
|
-
# layer = 1, 2, or 3
|
273
|
-
attr_reader(:layer)
|
274
|
-
|
275
|
-
# bitrate in kbps
|
276
|
-
attr_reader(:bitrate)
|
277
|
-
|
278
|
-
# samplerate in Hz
|
279
|
-
attr_reader(:samplerate)
|
280
|
-
|
281
|
-
# channel mode => "Stereo", "JStereo", "Dual Channel" or "Single Channel"
|
282
|
-
attr_reader(:channel_mode)
|
283
|
-
|
284
|
-
# variable bitrate => true or false
|
285
|
-
attr_reader(:vbr)
|
286
|
-
|
287
|
-
# length in seconds as a Float
|
288
|
-
attr_reader(:length)
|
289
|
-
|
290
|
-
# error protection => true or false
|
291
|
-
attr_reader(:error_protection)
|
292
|
-
|
293
|
-
#a sort of "universal" tag, regardless of the tag version, 1 or 2, with the same keys as @tag1
|
294
|
-
attr_reader(:tag)
|
295
|
-
|
296
|
-
# id3v1 tag has a Hash. You can modify it, it will be written when calling
|
297
|
-
# "close" method.
|
298
|
-
attr_accessor(:tag1)
|
299
|
-
|
300
|
-
# id3v2 tag as a Hash
|
301
|
-
attr_reader(:tag2)
|
302
|
-
|
303
|
-
# the original filename
|
304
|
-
attr_reader(:filename)
|
305
|
-
|
306
|
-
# Moved hastag1? and hastag2? to be booleans
|
307
|
-
attr_reader(:hastag1, :hastag2)
|
308
|
-
|
309
|
-
# Test the presence of an id3v1 tag in file +filename+
|
310
|
-
def self.hastag1?(filename)
|
311
|
-
File.open(filename) { |f|
|
312
|
-
f.seek(-TAGSIZE, File::SEEK_END)
|
313
|
-
f.read(3) == "TAG"
|
314
|
-
}
|
315
|
-
end
|
316
|
-
|
317
|
-
# Test the presence of an id3v2 tag in file +filename+
|
318
|
-
def self.hastag2?(filename)
|
319
|
-
File.open(filename) { |f|
|
320
|
-
f.read(3) == "ID3"
|
321
|
-
}
|
322
|
-
end
|
323
|
-
|
324
|
-
|
325
|
-
# Remove id3v1 tag from +filename+
|
326
|
-
def self.removetag1(filename)
|
327
|
-
if self.hastag1?(filename)
|
328
|
-
newsize = File.size(filename) - TAGSIZE
|
329
|
-
File.open(filename, "r+") { |f| f.truncate(newsize) }
|
330
|
-
end
|
331
|
-
end
|
332
|
-
|
333
|
-
# Instantiate a new Mp3Info object with name +filename+
|
334
|
-
def initialize(filename)
|
335
|
-
$stderr.puts("#{self.class}::new() does not take block; use #{self.class}::open() instead") if block_given?
|
336
|
-
raise(Mp3InfoError, "empty file") unless File.stat(filename).size? #FIXME
|
337
|
-
@filename = filename
|
338
|
-
@hastag1, @hastag2 = false
|
339
|
-
@tag = Hash.new
|
340
|
-
@tag1 = Hash.new
|
341
|
-
@tag2 = Hash.new
|
342
|
-
|
343
|
-
@file = File.new(filename, "rb")
|
344
|
-
parse_tags
|
345
|
-
@tag_orig = @tag1.dup
|
346
|
-
|
347
|
-
#creation of a sort of "universal" tag, regardless of the tag version
|
348
|
-
if hastag2?
|
349
|
-
h = {
|
350
|
-
"title" => "TIT2",
|
351
|
-
"artist" => "TPE1",
|
352
|
-
"album" => "TALB",
|
353
|
-
"year" => "TYER",
|
354
|
-
"tracknum" => "TRCK",
|
355
|
-
"comments" => "COMM",
|
356
|
-
"genre" => 255,
|
357
|
-
"genre_s" => "TCON"
|
358
|
-
}
|
359
|
-
|
360
|
-
h.each { |k, v| @tag[k] = @tag2[v] }
|
361
|
-
|
362
|
-
elsif hastag1?
|
363
|
-
@tag = @tag1.dup
|
364
|
-
end
|
365
|
-
|
366
|
-
|
367
|
-
### extracts MPEG info from MPEG header and stores it in the hash @mpeg
|
368
|
-
### head (fixnum) = valid 4 byte MPEG header
|
369
|
-
|
370
|
-
found = false
|
371
|
-
|
372
|
-
5.times do
|
373
|
-
head = find_next_frame()
|
374
|
-
@mpeg_version = [2, 1][head[19]]
|
375
|
-
@layer = LAYER[head.bits(18,17)]
|
376
|
-
next if @layer.nil?
|
377
|
-
@bitrate = BITRATE[@mpeg_version-1][@layer-1][head.bits(15,12)-1]
|
378
|
-
@error_protection = head[16] == 0 ? true : false
|
379
|
-
@samplerate = SAMPLERATE[@mpeg_version-1][head.bits(11,10)]
|
380
|
-
@padding = (head[9] == 1 ? true : false)
|
381
|
-
@channel_mode = CHANNEL_MODE[@channel_num = head.bits(7,6)]
|
382
|
-
@copyright = (head[3] == 1 ? true : false)
|
383
|
-
@original = (head[2] == 1 ? true : false)
|
384
|
-
@vbr = false
|
385
|
-
found = true
|
386
|
-
break
|
387
|
-
end
|
388
|
-
|
389
|
-
raise(Mp3InfoError, "Cannot find good frame") unless found
|
390
|
-
|
391
|
-
|
392
|
-
seek = @mpeg_version == 1 ?
|
393
|
-
(@channel_num == 3 ? 17 : 32) :
|
394
|
-
(@channel_num == 3 ? 9 : 17)
|
395
|
-
|
396
|
-
@file.seek(seek, IO::SEEK_CUR)
|
397
|
-
|
398
|
-
vbr_head = @file.read(4)
|
399
|
-
if vbr_head == "Xing"
|
400
|
-
flags = @file.get32bits
|
401
|
-
@streamsize = @frames = 0
|
402
|
-
flags[1] == 1 and @frames = @file.get32bits
|
403
|
-
flags[2] == 1 and @streamsize = @file.get32bits
|
404
|
-
# currently this just skips the TOC entries if they're found
|
405
|
-
@file.seek(100, IO::SEEK_CUR) if flags[0] == 1
|
406
|
-
@vbr_quality = @file.get32bits if flags[3] == 1
|
407
|
-
@length = (26/1000.0)*@frames
|
408
|
-
@bitrate = (((@streamsize/@frames)*@samplerate)/144) >> 10
|
409
|
-
@vbr = true
|
410
|
-
else
|
411
|
-
# for cbr, calculate duration with the given bitrate
|
412
|
-
@streamsize = @file.stat.size - (@hastag1 ? TAGSIZE : 0) - (@hastag2 ? @tag2["length"] : 0)
|
413
|
-
@length = ((@streamsize << 3)/1000.0)/@bitrate
|
414
|
-
if @tag2["TLEN"]
|
415
|
-
# but if another duration is given and it isn't close (within 5%)
|
416
|
-
# assume the mp3 is vbr and go with the given duration
|
417
|
-
tlen = (@tag2["TLEN"].to_i)/1000
|
418
|
-
percent_diff = ((@length.to_i-tlen)/tlen.to_f)
|
419
|
-
if percent_diff.abs > 0.05
|
420
|
-
# without the xing header, this is the best guess without reading
|
421
|
-
# every single frame
|
422
|
-
@vbr = true
|
423
|
-
@length = @tag2["TLEN"].to_i/1000
|
424
|
-
@bitrate = (@streamsize / @bitrate) >> 10
|
425
|
-
end
|
426
|
-
end
|
427
|
-
end
|
428
|
-
end
|
429
|
-
|
430
|
-
# "block version" of Mp3Info::new()
|
431
|
-
def self.open(filename)
|
432
|
-
m = self.new(filename)
|
433
|
-
ret = nil
|
434
|
-
if block_given?
|
435
|
-
begin
|
436
|
-
ret = yield(m)
|
437
|
-
ensure
|
438
|
-
m.close
|
439
|
-
end
|
440
|
-
else
|
441
|
-
ret = m
|
442
|
-
end
|
443
|
-
ret
|
444
|
-
end
|
445
|
-
|
446
|
-
# Remove id3v1 from mp3
|
447
|
-
def removetag1
|
448
|
-
if hastag1?
|
449
|
-
newsize = @file.stat.size(filename) - TAGSIZE
|
450
|
-
@file.truncate(newsize)
|
451
|
-
@tag1.clear
|
452
|
-
end
|
453
|
-
self
|
454
|
-
end
|
455
|
-
|
456
|
-
# Has file an id3v1 or v2 tag? true or false
|
457
|
-
def hastag?
|
458
|
-
@hastag1 or @hastag2
|
459
|
-
end
|
460
|
-
|
461
|
-
# Has file an id3v1 tag? true or false
|
462
|
-
def hastag1?
|
463
|
-
@hastag1
|
464
|
-
end
|
465
|
-
|
466
|
-
# Has file an id3v2 tag? true or false
|
467
|
-
def hastag2?
|
468
|
-
@hastag2
|
469
|
-
end
|
470
|
-
|
471
|
-
|
472
|
-
# Flush pending modifications to tags and close the file
|
473
|
-
def close
|
474
|
-
return if @file.nil?
|
475
|
-
if @tag1 != @tag_orig
|
476
|
-
@tag_orig.update(@tag1)
|
477
|
-
#puts "@tag_orig: #{@tag_orig.inspect}"
|
478
|
-
@file.reopen(@filename, 'rb+')
|
479
|
-
@file.seek(-TAGSIZE, File::SEEK_END)
|
480
|
-
t = @file.read(3)
|
481
|
-
if t != 'TAG'
|
482
|
-
#append new tag
|
483
|
-
@file.seek(0, File::SEEK_END)
|
484
|
-
@file.write('TAG')
|
485
|
-
end
|
486
|
-
str = [
|
487
|
-
@tag_orig["title"]||"",
|
488
|
-
@tag_orig["artist"]||"",
|
489
|
-
@tag_orig["album"]||"",
|
490
|
-
((@tag_orig["year"] != 0) ? ("%04d" % @tag_orig["year"]) : "\0\0\0\0"),
|
491
|
-
@tag_orig["comments"]||"",
|
492
|
-
0,
|
493
|
-
@tag_orig["tracknum"]||0,
|
494
|
-
@tag_orig["genre"]||255
|
495
|
-
].pack("Z30Z30Z30Z4Z28CCC")
|
496
|
-
@file.write(str)
|
497
|
-
end
|
498
|
-
@file.close
|
499
|
-
@file = nil
|
500
|
-
end
|
501
|
-
|
502
|
-
# inspect inside Mp3Info
|
503
|
-
def to_s
|
504
|
-
s = "MPEG #{@mpeg_version} Layer #{@layer} #{@vbr ? "VBR" : "CBR"} #{@bitrate} Kbps #{@channel_mode} #{@samplerate} Hz length #{@length} sec. error protection #{@error_protection} "
|
505
|
-
s << "tag1: "+@tag1.inspect+"\n" if @hastag1
|
506
|
-
s << "tag2: "+@tag2.inspect+"\n" if @hastag2
|
507
|
-
s
|
508
|
-
end
|
509
|
-
|
510
|
-
|
511
|
-
private
|
512
|
-
|
513
|
-
### parses the id3 tags of the currently open @file
|
514
|
-
def parse_tags
|
515
|
-
return if @file.stat.size < TAGSIZE # file is too small
|
516
|
-
@file.seek(0)
|
517
|
-
f3 = @file.read(3)
|
518
|
-
gettag1 if f3 == "TAG" # v1 tag at beginning
|
519
|
-
gettag2 if f3 == "ID3" # v2 tag at beginning
|
520
|
-
unless @hastag1 # v1 tag at end
|
521
|
-
# this preserves the file pos if tag2 found, since gettag2 leaves
|
522
|
-
# the file at the best guess as to the first MPEG frame
|
523
|
-
pos = (@hastag2 ? @file.pos : 0)
|
524
|
-
# seek to where id3v1 tag should be
|
525
|
-
@file.seek(-TAGSIZE, IO::SEEK_END)
|
526
|
-
gettag1 if @file.read(3) == "TAG"
|
527
|
-
@file.seek(pos)
|
528
|
-
end
|
529
|
-
end
|
530
|
-
|
531
|
-
### reads in id3 field strings, stripping out non-printable chars
|
532
|
-
### len (fixnum) = number of chars in field
|
533
|
-
### returns string
|
534
|
-
def read_id3_string(len)
|
535
|
-
#FIXME handle unicode strings
|
536
|
-
#return @file.read(len)
|
537
|
-
s = ""
|
538
|
-
len.times do
|
539
|
-
c = @file.getc
|
540
|
-
# only append printable characters
|
541
|
-
s << c if c >= 32 and c < 254
|
542
|
-
end
|
543
|
-
return s.strip
|
544
|
-
#return (s[0..2] == "eng" ? s[3..-1] : s)
|
545
|
-
end
|
546
|
-
|
547
|
-
### gets id3v1 tag information from @file
|
548
|
-
### assumes @file is pointing to char after "TAG" id
|
549
|
-
def gettag1
|
550
|
-
@hastag1 = true
|
551
|
-
@tag1["title"] = read_id3_string(30)
|
552
|
-
@tag1["artist"] = read_id3_string(30)
|
553
|
-
@tag1["album"] = read_id3_string(30)
|
554
|
-
year_t = read_id3_string(4).to_i
|
555
|
-
@tag1["year"] = year_t unless year_t == 0
|
556
|
-
comments = @file.read(30)
|
557
|
-
if comments[-2] == 0
|
558
|
-
@tag1["tracknum"] = comments[-1].to_i
|
559
|
-
comments.chop! #remove the last char
|
560
|
-
end
|
561
|
-
#@tag1["comments"] = comments.sub!(/\0.*$/, '')
|
562
|
-
@tag1["comments"] = comments.strip
|
563
|
-
@tag1["genre"] = @file.getc
|
564
|
-
@tag1["genre_s"] = GENRES[@tag1["genre"]] || ""
|
565
|
-
end
|
566
|
-
|
567
|
-
### gets id3v2 tag information from @file
|
568
|
-
def gettag2
|
569
|
-
@file.seek(3)
|
570
|
-
version_maj, version_min, flags = @file.read(3).unpack("CCB4")
|
571
|
-
unsync, ext_header, experimental, footer = (0..3).collect { |i| flags[i].chr == '1' }
|
572
|
-
return unless [2, 3, 4].include?(version_maj)
|
573
|
-
@hastag2 = true
|
574
|
-
@tag2["version"] = "2.#{version_maj}.#{version_min}"
|
575
|
-
tag2_len = @file.get_syncsafe
|
576
|
-
case version_maj
|
577
|
-
when 2
|
578
|
-
read_id3v2_2_frames(tag2_len)
|
579
|
-
when 3,4
|
580
|
-
# seek past extended header if present
|
581
|
-
@file.seek(@file.get_syncsafe - 4, IO::SEEK_CUR) if ext_header
|
582
|
-
read_id3v2_3_frames(tag2_len)
|
583
|
-
end
|
584
|
-
tag2["length"] = @file.pos
|
585
|
-
# we should now have @file sitting at the first MPEG frame
|
586
|
-
end
|
587
|
-
|
588
|
-
### runs thru @file one char at a time looking for best guess of first MPEG
|
589
|
-
### frame, which should be first 0xff byte after id3v2 padding zero's
|
590
|
-
### returns true
|
591
|
-
def v2_end?
|
592
|
-
until @file.getc == 0xff
|
593
|
-
end
|
594
|
-
@file.seek(-1, IO::SEEK_CUR)
|
595
|
-
true
|
596
|
-
end
|
597
|
-
|
598
|
-
### reads id3 ver 2.3.x/2.4.x frames and adds the contents to @tag2 hash
|
599
|
-
### tag2_len (fixnum) = length of entire id3v2 data, as reported in header
|
600
|
-
### NOTE: the id3v2 header does not take padding zero's into consideration
|
601
|
-
def read_id3v2_3_frames(tag2_len)
|
602
|
-
v2end_found = false
|
603
|
-
until v2end_found # there are 2 ways to end the loop
|
604
|
-
name = @file.read(4)
|
605
|
-
if name[0] == 0
|
606
|
-
@file.seek(-4, IO::SEEK_CUR) # 1. find a padding zero,
|
607
|
-
v2end_found = v2_end? # so we seek to end of zeros
|
608
|
-
else
|
609
|
-
size = @file.get32bits
|
610
|
-
@file.seek(2, IO::SEEK_CUR) # skip flags
|
611
|
-
add_value_to_tag2(name, size)
|
612
|
-
# case name
|
613
|
-
# when /T[A-Z]+|COMM/
|
614
|
-
# data = read_id3_string(size-1)
|
615
|
-
# add_value_to_tag2(name, data)
|
616
|
-
# else
|
617
|
-
# @file.seek(size-1, IO::SEEK_CUR)
|
618
|
-
# end
|
619
|
-
v2end_found = true if @file.pos >= tag2_len # 2. reach length from header
|
620
|
-
end
|
621
|
-
end
|
622
|
-
end
|
623
|
-
|
624
|
-
### reads id3 ver 2.2.x frames and adds the contents to @tag2 hash
|
625
|
-
### tag2_len (fixnum) = length of entire id3v2 data, as reported in header
|
626
|
-
### NOTE: the id3v2 header does not take padding zero's into consideration
|
627
|
-
def read_id3v2_2_frames(tag2_len)
|
628
|
-
v2end_found = false
|
629
|
-
until v2end_found
|
630
|
-
name = @file.read(3)
|
631
|
-
if name[0] == 0
|
632
|
-
@file.seek(-3, IO::SEEK_CUR)
|
633
|
-
v2end_found = v2_end?
|
634
|
-
else
|
635
|
-
size = (@file.getc << 16) + (@file.getc << 8) + @file.getc
|
636
|
-
add_value_to_tag2(name, size)
|
637
|
-
v2end_found = true if @file.pos >= tag2_len
|
638
|
-
end
|
639
|
-
end
|
640
|
-
end
|
641
|
-
|
642
|
-
### Add data to tag2["name"]
|
643
|
-
### read lang_encoding, decode data if unicode and
|
644
|
-
### create an array if the key ever exists in the tag
|
645
|
-
def add_value_to_tag2(name, size)
|
646
|
-
lang_encoding = @file.getc # language encoding bit 0 for iso_8859_1, 1 for unicode
|
647
|
-
data = size == 0 ? "" : @file.read(size-1)
|
648
|
-
|
649
|
-
if lang_encoding == 1 and name[0] == ?T
|
650
|
-
require "iconv"
|
651
|
-
|
652
|
-
#strip byte-order bytes at the beginning of the unicode string if they exists
|
653
|
-
data[0..3] =~ /^[\xff\xfe]+$/ and data = data[2..-1]
|
654
|
-
|
655
|
-
data = Iconv.iconv("ISO-8859-1", "UNICODE", data)[0]
|
656
|
-
end
|
657
|
-
|
658
|
-
if @tag2.keys.include?(name)
|
659
|
-
unless @tag2[name].is_a?(Array)
|
660
|
-
keep = @tag2[name]
|
661
|
-
@tag2[name] = []
|
662
|
-
@tag2[name] << keep
|
663
|
-
end
|
664
|
-
@tag2[name] << data
|
665
|
-
else
|
666
|
-
@tag2[name] = data
|
667
|
-
end
|
668
|
-
end
|
669
|
-
|
670
|
-
### reads through @file from current pos until it finds a valid MPEG header
|
671
|
-
### returns the MPEG header as FixNum
|
672
|
-
def find_next_frame
|
673
|
-
# @file will now be sitting at the best guess for where the MPEG frame is.
|
674
|
-
# It should be at byte 0 when there's no id3v2 tag.
|
675
|
-
# It should be at the end of the id3v2 tag or the zero padding if there
|
676
|
-
# is a id3v2 tag.
|
677
|
-
start_pos = @file.pos
|
678
|
-
dummyproof = @file.stat.size - @file.pos
|
679
|
-
dummyproof.times do |i|
|
680
|
-
if @file.getc == 0xff
|
681
|
-
head = 0xff000000 + (@file.getc << 16) + (@file.getc << 8) + @file.getc
|
682
|
-
if check_head(head)
|
683
|
-
return head
|
684
|
-
else
|
685
|
-
@file.seek(-3, IO::SEEK_CUR)
|
686
|
-
end
|
687
|
-
end
|
688
|
-
end
|
689
|
-
raise Mp3InfoError
|
690
|
-
end
|
691
|
-
|
692
|
-
### checks the given header to see if it is valid
|
693
|
-
### head (fixnum) = 4 byte value to test for MPEG header validity
|
694
|
-
### returns true if valid, false if not
|
695
|
-
def check_head(head)
|
696
|
-
return false if head & 0xffe00000 != 0xffe00000 # 11 bit MPEG frame sync
|
697
|
-
return false if head & 0x00060000 == 0x00060000 # 2 bit layer type
|
698
|
-
return false if head & 0x0000f000 == 0x0000f000 # 4 bit bitrate
|
699
|
-
return false if head & 0x0000f000 == 0x00000000 # free format bitstream
|
700
|
-
return false if head & 0x00000c00 == 0x00000c00 # 2 bit frequency
|
701
|
-
return false if head & 0xffff0000 == 0xfffe0000
|
702
|
-
true
|
703
|
-
end
|
704
|
-
|
705
|
-
end
|
706
|
-
|
707
|
-
if $0 == __FILE__
|
708
|
-
while filename = ARGV.shift
|
709
|
-
begin
|
710
|
-
info = Mp3Info.new(filename)
|
711
|
-
puts filename
|
712
|
-
#puts "MPEG #{info.mpeg_version} Layer #{info.layer} #{info.vbr ? "VBR" : "CBR"} #{info.bitrate} Kbps \
|
713
|
-
#{info.channel_mode} #{info.samplerate} Hz length #{info.length} sec."
|
714
|
-
puts info
|
715
|
-
rescue Mp3InfoError => e
|
716
|
-
puts "#{filename}\nERROR: #{e}"
|
717
|
-
end
|
718
|
-
puts
|
719
|
-
end
|
720
|
-
end
|