root/tools/station.py @ b8048e2f8973fcb1cac10a71e11765d586267e7a

Revision b8048e2f8973fcb1cac10a71e11765d586267e7a, 21.1 KB (checked in by Guillaume Pellerin <yomguy@…>, 4 years ago)

add run mode, fix bad tests

  • Property mode set to 100644
Line 
1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3#
4# Copyright (C) 2006-2009 Guillaume Pellerin
5
6# <yomguy@parisson.com>
7
8# This software is a computer program whose purpose is to stream audio
9# and video data through icecast2 servers.
10
11# This software is governed by the CeCILL license under French law and
12# abiding by the rules of distribution of free software. You can use,
13# modify and/ or redistribute the software under the terms of the CeCILL
14# license as circulated by CEA, CNRS and INRIA at the following URL
15# "http://www.cecill.info".
16
17# As a counterpart to the access to the source code and  rights to copy,
18# modify and redistribute granted by the license, users are provided only
19# with a limited warranty and the software's author, the holder of the
20# economic rights, and the successive licensors have only limited
21# liability.
22
23# In this respect, the user's attention is drawn to the risks associated
24# with loading, using,  modifying and/or developing or reproducing the
25# software by the user in light of its specific status of free software,
26# that may mean that it is complicated to manipulate, and that also
27# therefore means that it is reserved for developers and  experienced
28# professionals having in-depth computer knowledge. Users are therefore
29# encouraged to load and test the software's suitability as regards their
30# requirements in conditions enabling the security of their systems and/or
31# data to be ensured and, more generally, to use and operate it in the
32# same conditions as regards security.
33
34# The fact that you are presently reading this means that you have had
35# knowledge of the CeCILL license and that you accept its terms.
36
37# Author: Guillaume Pellerin <yomguy@parisson.com>
38
39import os
40import sys
41import time
42import datetime
43import string
44import random
45import shout
46import tinyurl
47from threading import Thread
48from __init__ import *
49
50class Station(Thread):
51    """a DeeFuzzer shouting station thread"""
52
53    def __init__(self, station, q, logger, m3u):
54        Thread.__init__(self)
55        self.station = station
56        self.q = q
57        self.logger = logger
58        self.channel = shout.Shout()
59        self.id = 999999
60        self.counter = 0
61        self.command = 'cat '
62        self.delay = 0
63
64       # Media
65        self.media_dir = self.station['media']['dir']
66        self.channel.format = self.station['media']['format']
67        self.shuffle_mode = int(self.station['media']['shuffle'])
68        self.bitrate = self.station['media']['bitrate']
69        self.ogg_quality = self.station['media']['ogg_quality']
70        self.samplerate = self.station['media']['samplerate']
71        self.voices = self.station['media']['voices']
72
73        # RSS
74        self.rss_dir = self.station['rss']['dir']
75        self.rss_enclosure = self.station['rss']['enclosure']
76
77        # Infos
78        self.channel.url = self.station['infos']['url']
79        self.short_name = self.station['infos']['short_name']
80        self.channel.name = self.station['infos']['name'] + ' : ' + self.channel.url
81        self.channel.genre = self.station['infos']['genre']
82        self.channel.description = self.station['infos']['description']
83        self.base_name = self.rss_dir + os.sep + self.short_name + '_' + self.channel.format
84        self.rss_current_file = self.base_name + '_current.xml'
85        self.rss_playlist_file = self.base_name + '_playlist.xml'
86        self.m3u = m3u
87
88        # Server
89        self.channel.protocol = 'http'     # | 'xaudiocast' | 'icy'
90        self.channel.host = self.station['server']['host']
91        self.channel.port = int(self.station['server']['port'])
92        self.channel.user = 'source'
93        self.channel.password = self.station['server']['sourcepassword']
94        self.channel.mount = '/' + self.short_name + '.' + self.channel.format
95        self.channel.public = int(self.station['server']['public'])
96        self.channel.audio_info = { 'bitrate': self.bitrate,
97                                    'samplerate': self.samplerate,
98                                    'quality': self.ogg_quality,
99                                    'channels': self.voices,}
100        self.playlist = self.get_playlist()
101        self.lp = len(self.playlist)
102        self.channel.open()
103        self.channel_delay = self.channel.delay()
104
105        # Logging
106        self.logger.write_info('Opening ' + self.short_name + ' - ' + self.channel.name + \
107                ' (' + str(self.lp) + ' tracks)...')
108
109        self.metadata_relative_dir = 'metadata'
110        self.metadata_url = self.channel.url + '/rss/' + self.metadata_relative_dir
111        self.metadata_dir = self.rss_dir + os.sep + self.metadata_relative_dir
112        if not os.path.exists(self.metadata_dir):
113            os.makedirs(self.metadata_dir)
114
115        # The station's player
116        self.player = Player()
117        self.player_mode = 0
118
119        # Jingling between each media.
120        # mode = 0 means Off, mode = 1 means On
121        self.jingles_mode = 0
122        if 'jingles' in self.station:
123            self.jingles_mode =  int(self.station['jingles']['mode'])
124            self.jingles_shuffle = self.station['jingles']['shuffle']
125            self.jingles_dir = self.station['jingles']['dir']
126            if self.jingles_mode == 1:
127                self.jingles_callback('/jingles', [1])
128
129        # Relaying
130        # mode = 0 means Off, mode = 1 means On
131        self.relay_mode = 0
132        if 'relay' in self.station:
133            self.relay_mode = int(self.station['relay']['mode'])
134            self.relay_url = self.station['relay']['url']
135            if self.relay_mode == 1:
136                self.relay_callback('/relay', [1])
137
138        # Twitting
139        # mode = 0 means Off, mode = 1 means On
140        self.twitter_mode = 0
141        if 'twitter' in self.station:
142            self.twitter_mode = int(self.station['twitter']['mode'])
143            self.twitter_user = self.station['twitter']['user']
144            self.twitter_pass = self.station['twitter']['pass']
145            self.twitter_tags = self.station['twitter']['tags'].split(' ')
146            self.twitter = Twitter(self.twitter_user, self.twitter_pass)
147            if self.twitter_mode == 1:
148                self.twitter_callback('/twitter', [1])
149
150        # Recording
151        # mode = 0 means Off, mode = 1 means On
152        self.record_mode = 0
153        if 'record' in self.station:
154            self.record_mode = int(self.station['record']['mode'])
155            self.record_dir = self.station['record']['dir']
156            if self.record_mode == 1:
157                self.record_callback('/write', [1])
158
159        # Running
160        # mode = 0 means Off, mode = 1 means On
161        self.run_mode = 1
162
163        # OSCing
164        self.osc_control_mode = 0
165        # mode = 0 means Off, mode = 1 means On
166        if 'control' in self.station:
167            self.osc_control_mode = int(self.station['control']['mode'])
168            self.osc_port = self.station['control']['port']
169            if self.osc_control_mode == 1:
170                self.osc_controller = OSCController(self.osc_port)
171                self.osc_controller.start()
172                # OSC paths and callbacks
173                self.osc_controller.add_method('/media/next', 'i', self.media_next_callback)
174                self.osc_controller.add_method('/media/relay', 'i', self.relay_callback)
175                self.osc_controller.add_method('/twitter', 'i', self.twitter_callback)
176                self.osc_controller.add_method('/jingles', 'i', self.jingles_callback)
177                self.osc_controller.add_method('/record', 'i', self.record_callback)
178                self.osc_controller.add_method('/player', 'i', self.player_callback)
179                self.osc_controller.add_method('/run', 'i', self.run_callback)
180
181    def run_callback(self, path, value):
182        value = value[0]
183        self.run_mode = value
184        message = "Received OSC message '%s' with arguments '%d'" % (path, value)
185        self.logger.write_info(message)
186
187    def media_next_callback(self, path, value):
188        value = value[0]
189        self.next_media = value
190        message = "Received OSC message '%s' with arguments '%d'" % (path, value)
191        self.logger.write_info(message)
192
193    def relay_callback(self, path, value):
194        value = value[0]
195        if value == 1:
196            self.relay_mode = 1
197            self.player.start_relay(self.relay_url)
198        elif value == 0:
199            self.relay_mode = 0
200            self.player.stop_relay()
201        self.next_media = 1
202        message = "Received OSC message '%s' with arguments '%d'" % (path, value)
203        self.logger.write_info(message)
204        message = "Relaying : %s" % self.relay_url
205        self.logger.write_info(message)
206
207    def twitter_callback(self, path, value):
208        value = value[0]
209        self.twitter_mode = value
210        message = "Received OSC message '%s' with arguments '%d'" % (path, value)
211        self.m3u_tinyurl = tinyurl.create_one(self.channel.url + '/m3u/' + self.m3u.split(os.sep)[-1])
212        self.rss_tinyurl = tinyurl.create_one(self.channel.url + '/rss/' + self.rss_playlist_file.split(os.sep)[-1])
213        self.logger.write_info(message)
214
215    def jingles_callback(self, path, value):
216        value = value[0]
217        if value == 1:
218            self.jingles_list = self.get_jingles()
219            self.jingles_length = len(self.jingles_list)
220            self.jingle_id = 0
221        self.jingles_mode = value
222        message = "Received OSC message '%s' with arguments '%d'" % (path, value)
223        self.logger.write_info(message)
224
225    def record_callback(self, path, value):
226        value = value[0]
227        if value == 1:
228            self.rec_file = self.short_name + '-' + \
229              datetime.datetime.now().strftime("%x-%X").replace('/', '_') + '.' + self.channel.format
230            self.recorder = Recorder(self.record_dir)
231            self.recorder.open(self.rec_file)
232        elif value == 0:
233            self.recorder.close()
234            if self.channel.format == 'mp3':
235                media = Mp3(self.record_dir + os.sep + self.rec_file)
236            if self.channel.format == 'ogg':
237                media = Ogg(self.record_dir + os.sep + self.rec_file)
238            media.metadata = {'artist': self.artist, 'title': self.title, 'date': str(datetime.datetime.now().strftime("%Y"))}
239            media.write_tags()
240        self.record_mode = value
241        message = "Received OSC message '%s' with arguments '%d'" % (path, value)
242        self.logger.write_info(message)
243
244    def player_callback(self, path, value):
245        value = value[0]
246        self.player_mode = value
247        message = "Received OSC message '%s' with arguments '%d'" % (path, value)
248        self.logger.write_info(message)
249
250    def get_playlist(self):
251        file_list = []
252        for root, dirs, files in os.walk(self.media_dir):
253            for file in files:
254                s = file.split('.')
255                ext = s[len(s)-1]
256                if ext.lower() == self.channel.format and not os.sep+'.' in file:
257                    file_list.append(root + os.sep + file)
258        file_list.sort()
259        return file_list
260
261    def get_jingles(self):
262        file_list = []
263        for root, dirs, files in os.walk(self.jingles_dir):
264            for file in files:
265                s = file.split('.')
266                ext = s[len(s)-1]
267                if ext.lower() == self.channel.format and not os.sep+'.' in file:
268                    file_list.append(root + os.sep + file)
269        file_list.sort()
270        return file_list
271
272    def get_next_media(self):
273        # Init playlist
274        if self.lp != 0:
275            old_playlist = self.playlist
276            new_playlist = self.get_playlist()
277            lp_new = len(new_playlist)
278
279            if lp_new != self.lp or self.counter == 0:
280                # Init playlists
281                self.playlist = new_playlist
282                self.id = 0
283                self.lp = lp_new
284
285                # Twitting new tracks
286                new_playlist_set = set(self.playlist)
287                old_playlist_set = set(old_playlist)
288                new_tracks = new_playlist_set - old_playlist_set
289
290                if len(new_tracks) != 0:
291                    self.new_tracks = list(new_tracks.copy())
292                    new_tracks_objs = self.media_to_objs(self.new_tracks)
293
294                    for media_obj in new_tracks_objs:
295                        title = media_obj.metadata['title']
296                        artist = media_obj.metadata['artist']
297                        if not (title or artist):
298                            song = str(media_obj.file_name)
299                        else:
300                            song = artist + ' : ' + title
301                        song = song.encode('utf-8')
302                        artist = artist.encode('utf-8')
303                        if self.twitter_mode == 1:
304                            artist_names = artist.split(' ')
305                            artist_tags = ' #'.join(list(set(artist_names)-set(['&', '-'])))
306                            message = '#newtrack ! %s #%s on #%s RSS : ' % (song.replace('_', ' '), artist_tags, self.short_name)
307                            message = message[:113] + self.rss_tinyurl
308                            self.update_twitter(message)
309
310                if self.shuffle_mode == 1:
311                    # Shake it, Fuzz it !
312                    random.shuffle(self.playlist)
313
314                self.logger.write_info('Station ' + self.short_name + \
315                                 ' : generating new playlist (' + str(self.lp) + ' tracks)')
316                self.update_rss(self.media_to_objs(self.playlist), self.rss_playlist_file, '(playlist)')
317
318            if self.jingles_mode == 1 and (self.counter % 2) == 0 and not self.jingles_length == 0:
319                media = self.jingles_list[self.jingle_id]
320                self.jingle_id = (self.jingle_id + 1) % self.jingles_length
321            else:
322                media = self.playlist[self.id]
323                self.id = (self.id + 1) % self.lp
324            return media
325        else:
326            mess = 'No media in media_dir !'
327            self.logger.write_error(mess)
328            sys.exit(mess)
329
330    def media_to_objs(self, media_list):
331        media_objs = []
332        for media in media_list:
333            file_name, file_title, file_ext = get_file_info(media)
334            if file_ext.lower() == 'mp3':
335                media_objs.append(Mp3(media))
336            elif file_ext.lower() == 'ogg':
337                media_objs.append(Ogg(media))
338        return media_objs
339
340    def update_rss(self, media_list, rss_file, sub_title):
341        rss_item_list = []
342        if not os.path.exists(self.rss_dir):
343            os.makedirs(self.rss_dir)
344        channel_subtitle = self.channel.name + ' ' + sub_title
345        _date_now = datetime.datetime.now()
346        date_now = str(_date_now)
347        media_absolute_playtime = _date_now
348
349        for media in media_list:
350            media_stats = os.stat(media.media)
351            media_date = time.localtime(media_stats[8])
352            media_date = time.strftime("%a, %d %b %Y %H:%M:%S +0200", media_date)
353            media.metadata['Duration'] = str(media.length).split('.')[0]
354            media.metadata['Bitrate'] = str(media.bitrate) + ' kbps'
355            media.metadata['Next play'] = str(media_absolute_playtime).split('.')[0]
356
357            media_description = '<table>'
358            media_description_item = '<tr><td>%s:   </td><td><b>%s</b></td></tr>'
359            for key in media.metadata.keys():
360                if media.metadata[key] != '':
361                    media_description += media_description_item % (key.capitalize(), media.metadata[key])
362            media_description += '</table>'
363
364            title = media.metadata['title']
365            artist = media.metadata['artist']
366            if not (title or artist):
367                song = str(media.file_title)
368            else:
369                song = artist + ' : ' + title
370
371            media_absolute_playtime += media.length
372
373            if self.rss_enclosure == '1':
374                media_link = self.channel.url + '/media/' + media.file_name
375                media_link = media_link.decode('utf-8')
376                rss_item_list.append(RSSItem(
377                    title = song,
378                    link = media_link,
379                    description = media_description,
380                    enclosure = Enclosure(media_link, str(media.size), 'audio/mpeg'),
381                    guid = Guid(media_link),
382                    pubDate = media_date,)
383                    )
384            else:
385                media_link = self.metadata_url + '/' + media.file_name + '.xml'
386                media_link = media_link.decode('utf-8')
387                rss_item_list.append(RSSItem(
388                    title = song,
389                    link = media_link,
390                    description = media_description,
391                    guid = Guid(media_link),
392                    pubDate = media_date,)
393                    )
394
395        rss = RSS2(title = channel_subtitle,
396                            link = self.channel.url,
397                            description = self.channel.description.decode('utf-8'),
398                            lastBuildDate = date_now,
399                            items = rss_item_list,)
400        f = open(rss_file, 'w')
401        rss.write_xml(f, 'utf-8')
402        f.close()
403
404    def update_twitter(self, message):
405        try:
406            self.twitter.post(message.decode('utf8'))
407            self.logger.write_info('Twitting : "' + message + '"')
408        except:
409            self.logger.write_error('Twitting : "' + message + '"')
410            pass
411
412    def set_relay_mode(self):
413        self.prefix = '#nowplaying (relaying #LIVE)'
414        song = self.relay_url
415        self.song = song.encode('utf-8')
416        self.artist = 'Various'
417        self.channel.set_metadata({'song': self.short_name + ' relaying : ' + self.song, 'charset': 'utf8',})
418        self.stream = self.player.relay_read()
419
420    def set_read_mode(self):
421        self.prefix = '#nowplaying'
422        self.current_media_obj = self.media_to_objs([self.media])
423        self.title = self.current_media_obj[0].metadata['title']
424        self.artist = self.current_media_obj[0].metadata['artist']
425        self.title = self.title.replace('_', ' ')
426        self.artist = self.artist.replace('_', ' ')
427        if not (self.title or self.artist):
428            song = str(self.current_media_obj[0].file_name)
429        else:
430            song = self.artist + ' : ' + self.title
431        self.song = song.encode('utf-8')
432        self.artist = self.artist.encode('utf-8')
433        self.metadata_file = self.metadata_dir + os.sep + self.current_media_obj[0].file_name + '.xml'
434        self.update_rss(self.current_media_obj, self.metadata_file, '')
435        self.channel.set_metadata({'song': self.song, 'charset': 'utf8',})
436        self.update_rss(self.current_media_obj, self.rss_current_file, '(currently playing)')
437        self.logger.write_info('Deefuzzing on %s :  id = %s, name = %s' \
438            % (self.short_name, self.id, self.current_media_obj[0].file_name))
439        self.player.set_media(self.media)
440        if self.player_mode == 0:
441            self.stream = self.player.file_read_slow()
442        elif self.player_mode == 1:
443            self.stream = self.player.file_read_fast()
444
445    def run(self):
446        while self.run_mode:
447            self.q.get(1)
448            self.next_media = 0
449            self.media = self.get_next_media()
450            self.counter += 1
451
452            if self.relay_mode:
453                self.set_relay_mode()
454            elif os.path.exists(self.media) and not os.sep+'.' in self.media:
455                if self.lp == 0:
456                    self.logger.write_error('Station ' + self.short_name + ' has no media to stream !')
457                    break
458                self.set_read_mode()
459            self.q.task_done()
460
461            self.q.get(1)
462            if (not (self.jingles_mode and (self.counter % 2)) or self.relay_mode) and self.twitter_mode:
463                artist_names = self.artist.split(' ')
464                artist_tags = ' #'.join(list(set(artist_names)-set(['&', '-'])))
465                message = '♫ %s %s on #%s #%s' % (self.prefix, self.song, self.short_name, artist_tags)
466                tags = '#' + ' #'.join(self.twitter_tags)
467                message = message + ' ' + tags
468                message = message[:107] + ' M3U : ' + self.m3u_tinyurl
469                self.update_twitter(message)
470            self.q.task_done()
471
472            for self.chunk in self.stream:
473                self.q.get(1)
474                if self.next_media or not self.run_mode:
475                    break
476                try:
477                    self.channel.send(self.chunk)
478                    self.channel.sync()
479                except:
480                    self.channel.close()
481                    self.logger.write_error('Station ' + self.short_name + ' : could not send the buffer to the server ')
482                    try:
483                        self.channel.open()
484                    except:
485                        self.logger.write_error('Station ' + self.short_name + ' : could connect to the server ')
486                        continue
487                    continue
488                try:
489                    if self.record_mode:
490                        self.recorder.write(self.chunk)
491                except:
492                    self.logger.write_error('Station ' + self.short_name + ' : could not write the buffer to the file ')
493                    continue
494                self.q.task_done()
495
496        self.channel.close()
Note: See TracBrowser for help on using the browser.