2014-01-07 20:29:40 +00:00
module JamRuby
class IcecastMount < ActiveRecord :: Base
2014-01-21 14:51:03 +00:00
@@log = Logging . logger [ IcecastMount ]
2014-01-19 02:20:44 +00:00
attr_accessible :authentication_id , :name , :source_username , :source_pass , :max_listeners , :max_listener_duration ,
:dump_file , :intro , :fallback_mount , :fallback_override , :fallback_when_full , :charset , :is_public ,
:stream_name , :stream_description , :stream_url , :genre , :bitrate , :mime_type , :subtype , :burst_size ,
2014-01-21 14:51:03 +00:00
:mp3_metadata_interval , :hidden , :on_connect , :on_disconnect ,
:music_session_id , :icecast_server_id , :icecast_mount_template_id , :listeners , :sourced ,
:sourced_needs_changing_at , as : :admin
2014-01-10 21:02:52 +00:00
2014-12-18 21:13:55 +00:00
attr_accessor :no_config_changed
2014-01-19 02:20:44 +00:00
belongs_to :authentication , class_name : " JamRuby::IcecastUserAuthentication " , inverse_of : :mount , :foreign_key = > 'authentication_id'
2014-05-06 13:34:38 +00:00
belongs_to :music_session , class_name : " JamRuby::ActiveMusicSession " , inverse_of : :mount , foreign_key : 'music_session_id'
2014-01-19 02:20:44 +00:00
2014-01-21 14:51:03 +00:00
belongs_to :server , class_name : " JamRuby::IcecastServer " , inverse_of : :mounts , foreign_key : 'icecast_server_id'
belongs_to :mount_template , class_name : " JamRuby::IcecastMountTemplate " , inverse_of : :mounts , foreign_key : 'icecast_mount_template_id'
2014-01-19 02:20:44 +00:00
2016-07-17 15:16:27 +00:00
has_many :source_changes , - > { order ( 'created_at DESC' ) } , class_name : " JamRuby::IcecastSourceChange " , inverse_of : :mount , foreign_key : 'icecast_mount_id'
2014-12-18 21:13:55 +00:00
2014-01-21 14:51:03 +00:00
validates :name , presence : true , uniqueness : true
2014-01-19 02:20:44 +00:00
validates :source_username , length : { minimum : 5 } , if : lambda { | m | m . source_username . present? }
validates :source_pass , length : { minimum : 5 } , if : lambda { | m | m . source_pass . present? }
2014-01-17 04:51:19 +00:00
validates :max_listeners , length : { in : 1 .. 15000 } , if : lambda { | m | m . max_listeners . present? }
validates :max_listener_duration , length : { in : 1 .. 3600 * 48 } , if : lambda { | m | m . max_listener_duration . present? }
2014-01-19 02:20:44 +00:00
validates :fallback_override , :inclusion = > { :in = > [ 0 , 1 ] } , if : lambda { | m | m . fallback_mount . present? }
validates :fallback_when_full , :inclusion = > { :in = > [ 0 , 1 ] } , if : lambda { | m | m . fallback_mount . present? }
2014-01-17 04:51:19 +00:00
validates :is_public , presence : true , :inclusion = > { :in = > [ - 1 , 0 , 1 ] }
validates :bitrate , numericality : { only_integer : true } , if : lambda { | m | m . bitrate . present? }
validates :burst_size , numericality : { only_integer : true } , if : lambda { | m | m . burst_size . present? }
validates :mp3_metadata_interval , numericality : { only_integer : true } , if : lambda { | m | m . mp3_metadata_interval . present? }
2014-01-19 02:20:44 +00:00
validates :hidden , :inclusion = > { :in = > [ 0 , 1 ] }
2014-01-21 14:51:03 +00:00
validates :server , presence : true
validate :name_has_correct_format
2014-01-19 02:20:44 +00:00
2014-01-21 14:51:03 +00:00
before_save :sanitize_active_admin
after_save :after_save
2014-12-18 21:13:55 +00:00
#after_save :poke_config
2014-01-21 14:51:03 +00:00
before_destroy :poke_config
2014-01-19 02:20:44 +00:00
def name_has_correct_format
errors . add ( :name , " must start with / " ) unless name && name . start_with? ( '/' )
end
2014-01-21 14:51:03 +00:00
def poke_config
server . update_attribute ( :config_changed , 1 ) if server
end
2014-01-19 02:20:44 +00:00
def after_save
2014-12-18 21:13:55 +00:00
server . update_attribute ( :config_changed , 1 ) unless no_config_changed
2014-01-19 02:20:44 +00:00
if ! sourced_was && sourced
2014-01-21 14:51:03 +00:00
# went from NOT SOURCED to SOURCED
notify_source_up
elsif sourced_was && ! sourced
# went from SOURCED to NOT SOURCED
notify_source_down
end
2014-12-18 21:13:55 +00:00
if source_direction_was != source_direction
# temporarily removed; it seems better to leave all the data in for now. It should never be that much
# if the requested source direction has changed, then delete diagnostic info
#IcecastSourceChange.delete_all(["icecast_mount_id = ?", self.id]) if source_direction
# and tell anyone listening that the direction has changed
2014-12-19 20:00:49 +00:00
# SubscriptionMessage.mount_source_direction(self)
2014-01-19 02:20:44 +00:00
end
2014-01-21 14:51:03 +00:00
# Note:
# Notification.send_source_down_requested does not occur here.
# we set up a cron that checks for streams that have not been successfully source up/down (after timeout ) in IcecastSourceCheck
2014-01-19 02:20:44 +00:00
end
def sanitize_active_admin
self . authentication_id = nil if self . authentication_id == ''
2014-01-21 14:51:03 +00:00
self . music_session_id = nil if self . music_session_id == ''
self . icecast_server_id = nil if self . icecast_server_id == ''
end
# creates a templated
2014-05-06 21:17:26 +00:00
def self . build_session_mount ( music_session , active_music_session , icecast_server )
2014-01-21 14:51:03 +00:00
# only public sessions get mounts currently
return nil unless music_session . fan_access
mount = nil
if icecast_server && icecast_server . mount_template_id
# we have a server with an associated mount_template; we can create a mount automatically
2014-05-06 21:17:26 +00:00
mount = icecast_server . mount_template . build_session_mount ( music_session , active_music_session )
2014-01-21 14:51:03 +00:00
mount . server = icecast_server
end
mount
2014-01-19 02:20:44 +00:00
end
2014-12-18 21:13:55 +00:00
def fail_state ( reason , detail = nil )
{ success : false , reason : reason , detail : detail }
end
def success_state ( reason , detail = nil )
{ success : true , reason : reason , detail : detail }
end
# success messages:
# * source_up - we are broadcasting - detail is the source user or empty. if empty you should refresh state in a few seconds
# * source_down - we are broadcasting - detail is the last source user, or empty. if empty you should refresh state in a few seconds
# * transition_up/down - a source change occurred recently, and we don't yet have any info from the client. you should refresh state in a few seconds
#
# failure messages:
# * multiple_clients
# * unknown - represents a code error
# * transition_timeout_up - a source change for up has occurred, but no clients have reported any info
# * transition_timeout_down - a source change for up has occurred, but no clients have reported any info
# * source_wrong_up - the source should be up, but it could not succeed according to the most recent effort.
# * source_wrong_down - the source should be down, but it could not succeed according to the most recent effort.
#
# for both source_wrong_up and source_wrong_down, valid detail values:
# * 'no_client' if no client has said anything to a request after some amount of time, or otherwise it's client defined
# client-defined reason values
# * 'initialize_singleton' - code error in the client
# * 'initialize_thread' - code error in the client
# * 'initialize_ogg' - could not initialize ogg encoder... likely code error
# * 'initialize_mp3' - could not initialize mp3 encoder... likely code error
# * 'initialize_socket' - could not initialize socket... likely code error
# * 'icecast_response' - icecast was not accessible or returned an error
# * TODO client defined
def state
begin
result = fail_state ( 'unknown' )
if sourced == should_source?
first = source_changes . first
# don't check source_changes if actual source state mirrors desired source state... just say we are good, and pass down relevant sourcing user ID if present
2014-12-19 20:00:49 +00:00
result = success_state ( 'source_' + ( sourced ? 'up' : 'down' ) , first . nil? ? nil : first . user_id )
2014-12-18 21:13:55 +00:00
elsif source_changes . count > 0
# if the desired source direction is up, but we haven't sourced yet... let's try and find out why
# let's first see if we have N clients contributing, which is a code error condition
clients = Hash [ source_changes . map { | source_change | [ source_change . client_id , source_change ] } ]
#if clients.length > 1
# this means more than one client has contributed... this is an code error condition
# result = fail_state('multiple_clients', clients)
#else
first = source_changes . first
if first . source_direction == should_source?
if first . success?
# the last message from any client indicated we had the right source
# if less than a second has passed, this is not strange. But more than that, it's strange
if sourced_needs_changing_at . nil? || ( Time . now - first . created_at < APP_CONFIG . source_changes_missing_secs )
result = success_state ( 'transition_' + ( source_direction ? 'up' : 'down' ) )
else
result = fail_state ( 'transition_timeout_' + ( source_direction ? 'up' : 'down' ) )
end
else
# so the last state indicated by the client agrees that our source info is wrong; we can use it's data to augment the frontend
result = fail_state ( 'source_wrong_' + ( source_direction ? 'up' : 'down' ) , first . reason )
end
else
# if the last message from the client is for the wrong source direction (meaning no attempt to change source state by client to the correct state)
# then report this info
if sourced_needs_changing_at . nil? || ( Time . now - sourced_needs_changing_at < APP_CONFIG . source_changes_missing_secs )
result = fail_state ( 'transition_' + ( source_direction ? 'up' : 'down' ) )
else
result = fail_state ( 'source_wrong_' + ( source_direction ? 'up' : 'down' ) , 'no_client' ) # no client implies no client has attempted to source
end
end
#end
else
# we have the wrong source direction, but no source change info.
# if less than a second has passed, this is not strange. But more than that, it's strange
if sourced_needs_changing_at . nil?
#result = fail_state('db_error', 'sourced_needs_changing_at is nil')
result = success_state ( 'transition_' + ( source_direction ? 'up' : 'down' ) )
elsif Time . now - sourced_needs_changing_at < APP_CONFIG . source_changes_missing_secs
result = success_state ( 'transition_' + ( source_direction ? 'up' : 'down' ) , 'initial' )
else
result = fail_state ( 'transition_timeout_' + ( source_direction ? 'up' : 'down' ) , 'initial' )
end
end
rescue Exception = > e
@@log . error ( " exception in IcecastMount.state #{ e } " )
end
result
end
2014-01-19 02:20:44 +00:00
def source_up
with_lock do
self . sourced = true
2014-12-19 20:00:49 +00:00
self . sourced_needs_changing_at = Time . now
2014-12-18 21:13:55 +00:00
self . no_config_changed = true
2014-01-21 14:51:03 +00:00
save ( validate : false )
2014-01-19 02:20:44 +00:00
end
end
def source_down
with_lock do
2014-01-21 14:51:03 +00:00
self . sourced = false
2014-12-19 20:00:49 +00:00
self . sourced_needs_changing_at = Time . now
2014-12-18 21:13:55 +00:00
self . no_config_changed = true
2014-01-21 14:51:03 +00:00
save ( validate : false )
2014-01-19 02:20:44 +00:00
end
end
2014-12-18 21:13:55 +00:00
def should_source?
self . listeners > 0
end
2014-01-19 02:20:44 +00:00
def listener_add
with_lock do
2014-12-18 21:13:55 +00:00
if listeners == 0 && ! sourced && ( self . sourced_needs_changing_at . nil? || Time . now - self . sourced_needs_changing_at > APP_CONFIG . source_changes_missing_secs )
# enough time has elapsed since the last time the source direction changed to instaneously request a source up
# listener count went above 0 and there is no source. ask the musician clients to source
notify_source_up_requested
end
2014-01-21 14:51:03 +00:00
# this is completely unsafe without that 'with_lock' statement above
self . listeners = self . listeners + 1
2014-12-18 21:13:55 +00:00
self . no_config_changed = true
2014-01-21 14:51:03 +00:00
save ( validate : false )
2014-01-19 02:20:44 +00:00
end
end
def listener_remove
2014-01-21 14:51:03 +00:00
if listeners == 0
@@log . warn ( " listeners is at 0, but we are being asked to remove a listener. maybe we missed a listener_add request earlier " )
return
end
2014-01-19 02:20:44 +00:00
with_lock do
2014-02-27 18:38:02 +00:00
self . sourced_needs_changing_at = Time . now if listeners == 1
2014-01-21 14:51:03 +00:00
# this is completely unsafe without that 'with_lock' statement above
self . listeners = self . listeners - 1
2014-12-18 21:13:55 +00:00
self . no_config_changed = true
2014-01-21 14:51:03 +00:00
save ( validations : false )
2014-01-19 02:20:44 +00:00
end
end
2014-01-14 21:22:05 +00:00
2014-01-21 14:51:03 +00:00
def notify_source_up_requested
2014-12-18 21:13:55 +00:00
if music_session_id
self . sourced_needs_changing_at = Time . now
self . source_direction = true
self . no_config_changed = true
save!
source_change = IcecastSourceChange . new
source_change . source_direction = true
source_change . success = true
source_change . mount = self
source_change . change_type = IcecastSourceChange :: CHANGE_TYPE_MOUNT_UP_REQUEST
source_change . save!
Notification . send_source_up_requested ( music_session ,
2014-01-21 14:51:03 +00:00
server . hostname ,
server . pick_listen_socket ( :port ) ,
name ,
resolve_string ( :source_username ) ,
resolve_string ( :source_pass ) ,
2014-12-18 21:13:55 +00:00
resolve_int ( :bitrate ) )
SubscriptionMessage . mount_source_up_requested ( self )
end
2014-01-21 14:51:03 +00:00
end
def notify_source_down_requested
2014-12-18 21:13:55 +00:00
if music_session_id
self . sourced_needs_changing_at = Time . now
self . source_direction = false
self . no_config_changed = true
save!
source_change = IcecastSourceChange . new
source_change . source_direction = false
source_change . success = true
source_change . mount = self
source_change . change_type = IcecastSourceChange :: CHANGE_TYPE_MOUNT_DOWN_REQUEST
source_change . save!
Notification . send_source_down_requested ( music_session , name )
SubscriptionMessage . mount_source_down_requested ( self )
end
2014-01-21 14:51:03 +00:00
end
def notify_source_up
2014-12-18 21:13:55 +00:00
if music_session_id
Notification . send_source_up ( music_session )
end
2014-01-21 14:51:03 +00:00
end
def notify_source_down
2014-12-18 21:13:55 +00:00
if music_session_id
Notification . send_source_down ( music_session )
end
2014-01-21 14:51:03 +00:00
end
# Check if the icecast_mount specifies the value; if not, use the mount_template's value take effect
2014-01-17 04:51:19 +00:00
def dumpXml ( builder )
builder . tag! 'mount' do | mount |
2014-01-19 02:20:44 +00:00
mount . tag! 'mount-name' , name
2014-01-21 14:51:03 +00:00
mount . tag! 'username' , resolve_string ( :source_username ) if string_present? ( :source_username )
mount . tag! 'password' , resolve_string ( :source_pass ) if string_present? ( :source_pass )
mount . tag! 'max-listeners' , resolve_int ( :max_listeners ) if int_present? ( :max_listeners )
mount . tag! 'max-listener-duration' , resolve_string ( :max_listener_duration ) if int_present? ( :max_listener_duration )
mount . tag! 'dump-file' , resolve_string ( :dump_file ) if string_present? ( :dump_file )
mount . tag! 'intro' , resolve_string ( :intro ) if string_present? ( :intro )
mount . tag! 'fallback-mount' , resolve_string ( :fallback_mount ) if string_present? ( :fallback_mount )
mount . tag! 'fallback-override' , resolve_int ( :fallback_override ) if int_present? ( :fallback_override )
mount . tag! 'fallback-when-full' , resolve_int ( :fallback_when_full ) if int_present? ( :fallback_when_full )
mount . tag! 'charset' , resolve_string ( :charset ) if string_present? ( :charset )
mount . tag! 'public' , resolve_int ( :is_public ) if int_present? ( :is_public )
mount . tag! 'stream-name' , resolve_string ( :stream_name ) if string_present? ( :stream_name )
mount . tag! 'stream-description' , resolve_string ( :stream_description ) if string_present? ( :stream_description )
mount . tag! 'stream-url' , resolve_string ( :stream_url ) if string_present? ( :stream_url )
mount . tag! 'genre' , resolve_string ( :genre ) if string_present? ( :genre )
mount . tag! 'bitrate' , resolve_int ( :bitrate ) if int_present? ( :bitrate )
mount . tag! 'type' , resolve_string ( :mime_type ) if string_present? ( :mime_type )
mount . tag! 'subtype' , resolve_string ( :subtype ) if string_present? ( :subtype )
mount . tag! 'burst-size' , resolve_int ( :burst_size ) if int_present? ( :burst_size )
mount . tag! 'mp3-metadata-interval' , resolve_int ( :mp3_metadata_interval ) if int_present? ( :mp3_metadata_interval )
mount . tag! 'hidden' , resolve_int ( :hidden ) if int_present? ( :hidden )
mount . tag! 'on-connect' , resolve_string ( :on_connect ) if string_present? ( :on_connect )
mount . tag! 'on-disconnect' , resolve_string ( :on_disconnect ) if string_present? ( :on_disconnect )
2014-01-07 20:29:40 +00:00
2014-01-17 04:51:19 +00:00
authentication . dumpXml ( builder ) if authentication
end
end
2014-01-14 21:22:05 +00:00
2014-01-25 20:03:14 +00:00
def url
raise " Unassociated server to mount " if self . server . nil?
2014-01-14 21:22:05 +00:00
2014-01-25 20:03:14 +00:00
" http:// #{ server . hostname } : #{ server . pick_listen_socket ( :port ) } #{ self . name } "
2014-01-14 21:22:05 +00:00
end
2014-01-21 14:51:03 +00:00
def resolve_string ( field )
self [ field ] . present? ? self [ field ] : mount_template && mount_template [ field ]
end
def string_present? ( field )
val = resolve_string ( field )
val ? val . present? : false
end
def resolve_int ( field )
! self [ field ] . nil? ? self [ field ] : mount_template && mount_template [ field ]
end
def int_present? ( field )
resolve_int ( field )
end
2014-01-07 20:29:40 +00:00
end
end