-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdj.rb
More file actions
202 lines (169 loc) · 5.42 KB
/
dj.rb
File metadata and controls
202 lines (169 loc) · 5.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
#!/usr/local/bin/ruby
# stdlib
require 'rubygems'
require 'logger'
require 'yaml'
# extra
$LOAD_PATH.unshift("lib", "lib/sqlite3")
require 'shout'
require 'sqlite3'
class DJ
def initialize
@config = YAML::load_file('config.yml')
@log_folder = File.join(File.expand_path(File.dirname(__FILE__)), "log")
@db_folder = File.join(File.expand_path(File.dirname(__FILE__)), "db")
@shout = Shout.new
@shout.host = @config["icecast"]["server"]
@shout.port = @config["icecast"]["port"]
@shout.user = @config["icecast"]["username"]
@shout.pass = @config["icecast"]["password"]
@shout.mount = @config["icecast"]["mount"]
@shout.name = @config["radio_name"]
# format can be changed per-song, but defaults to MP3
@shout.format = Shout::MP3
FileUtils.mkdir_p(@db_folder)
@db = SQLite3::Database.new(File.join(@db_folder, "rubedo.db"))
@db.busy_timeout(200)
@mode = :user
if @config["dj_log_file"] and @config["dj_log_file"].any?
FileUtils.mkdir_p(@log_folder)
@log = Logger.new(File.join(@log_folder, @config["dj_log_file"]), 'daily')
@log.info("DJ started, begin logging.")
end
end
def start
connect
play_songs
end
def connect
begin
@shout.connect
rescue
log.fatal "Couldn't connect to Icecast server." if log
exit
end
end
def play_songs
loop do
song = next_song!
if song
play song
mark_done! song
else
play random_song
end
end
end
def play(song)
# a nil song means there are no songs at all, so just slowly loop until one comes along
if song.nil?
sleep 500
return nil
end
id, path, title = song
song_path = File.join music_folder, path
unless File.exists?(song_path)
log.error "File didn't exist, moving on to the next song. Full path was #{song_path}" if log
return
end
# set MP3 or OGG format
format = @shout.format
case File.extname(path)
when ".mp3"
format = Shout::MP3
when ".ogg"
format = Shout::OGG
else
format = Shout::MP3
end
if format != @shout.format
log.info "Switching stream formats, re-connecting." if log
@shout.disconnect
@shout.format = format
@shout.connect
end
# allow interrupts?
seek_interrupt = (@mode == :dj and @config["interrupt_empty_queue"])
File.open(song_path) do |file|
# set metadata (MP3 only)
if @shout.format == Shout::MP3
metadata = ShoutMetadata.new
metadata.add 'song', title
metadata.add 'filename', File.basename(path)
@shout.metadata = metadata
end
log.info "Playing #{title}!" if log
while data = file.read(16384)
begin
@shout.send data
break if seek_interrupt and next_song
@shout.sync
rescue
log.error "Error connecting to Icecast server. Don't worry, reconnecting." if log
@shout.disconnect
@shout.connect
end
end
end
end
# takes a random song off of the filesystem, making this source client independent of any web frontend
def random_song
@mode = :dj
wd = Dir.pwd
Dir.chdir music_folder
songs = Dir.glob("**/*.{mp3,ogg}")
path = songs[rand(songs.size)]
Dir.chdir wd
if path.blank?
nil
else
[0, path, quick_title(path)]
end
end
# This returns the filename of the next song to be played. By default, it will NOT also set this as the next song to be played. To do this, call next_song!
def next_song(set_as_playing = false)
@mode = :user
play_id, filename, title, song_id = nil
begin
# this query will get a song which was cut off while playing first, and failing that, will get the next song on the queue which hasn't been played
play_id, filename, title, song_id = db.get_first_row "select id, filename, title, song_id from rubedo_plays where queued_at IS NOT NULL order by queued_at asc limit 1"
return nil unless play_id
rescue
log.error "Error at some point during finding #{'(and setting) ' if set_as_playing}next song. Filename was: #{filename}\n#{$!}" if log
return nil
end
mark_start!(play_id, song_id) if set_as_playing
[play_id, filename, title]
end
def next_song!
next_song(true)
end
# these functions are only called when the song was queued by a user (@mode == :user)
def mark_start!(play_id, song_id)
begin
db.execute("update rubedo_plays set played_at = ?, queued_at = NULL WHERE id = ?", Time.now, play_id)
count = db.get_first_value("select play_count from rubedo_songs where id = ?", song_id)
db.execute("update rubedo_songs set last_played_at = ?, play_count = ?", Time.now, count + 1)
rescue
log.error "Error during marking a song as beginning. Song ID: #{song_id}"
end
end
def mark_done!(song)
begin
db.execute("delete from rubedo_plays where id = ?", song[0])
rescue
log.error "Error marking song as done. Play ID: #{song[0]}"
end
end
# getting the title of a song we may not have an entry for
def quick_title(song)
File.basename(song, File.extname(song)).gsub(/^[^A-Za-z]+\s+(\w)/, "\\1")
end
def log; @log; end
def db; @db; end
def music_folder
music = @config["music_folder"]
File.exists?(music) ? music : "./music"
end
end
DJ.new.start if __FILE__ == $0