id = intval($id); if ($info = $this->_get_info()) { foreach ($info as $key => $value) { $this->$key = $value; } $data = pathinfo($this->file); $this->type = strtolower($data['extension']); $this->mime = self::type_to_mime($this->type); } else { $this->id = null; return false; } return true; } // constructor /** * insert * * This inserts the song described by the passed array */ public static function insert($results) { $catalog = $results['catalog']; $file = $results['file']; $title = trim($results['title']) ?: $file; $artist = $results['artist']; $album = $results['album']; $bitrate = $results['bitrate']; $rate = $results['rate'] ?: 0; $mode = $results['mode']; $size = $results['size'] ?: 0; $time = $results['time'] ?: 0; $track = $results['track']; $track_mbid = $results['mb_trackid']; $album_mbid = $results['mb_albumid']; $artist_mbid = $results['mb_artistid']; $disk = $results['disk'] ?: 0; $year = $results['year'] ?: 0; $comment = $results['comment']; $tags = $results['genre']; // multiple genre support makes this an array $lyrics = $results['lyrics']; $artist_id = Artist::check($artist, $artist_mbid); $album_id = Album::check($album, $year, $disk, $album_mbid); $sql = 'INSERT INTO `song` (`file`, `catalog`, `album`, `artist`, ' . '`title`, `bitrate`, `rate`, `mode`, `size`, `time`, `track`, ' . '`addition_time`, `year`, `mbid`) ' . 'VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; $db_results = Dba::write($sql, array( $file, $catalog, $album_id, $artist_id, $title, $bitrate, $rate, $mode, $size, $time, $track, time(), $year, $track_mbid)); if (!$db_results) { debug_event('song', 'Unable to insert' . $file, 2); return false; } $song_id = Dba::insert_id(); if (is_array($tags)) { foreach ($tags as $tag) { $tag = trim($tag); Tag::add('song', $song_id, $tag, false); Tag::add('album', $album_id, $tag, false); Tag::add('artist', $artist_id, $tag, false); } } $sql = 'INSERT INTO `song_data` (`song_id`, `comment`, `lyrics`) ' . 'VALUES(?, ?, ?)'; Dba::write($sql, array($song_id, $comment, $lyrics)); return true; } /** * gc * * Cleans up the song_data table */ public static function gc() { Dba::write('DELETE FROM `song_data` USING `song_data` LEFT JOIN `song` ON `song`.`id` = `song_data`.`song_id` WHERE `song`.`id` IS NULL'); } /** * build_cache * * This attempts to reduce queries by asking for everything in the * browse all at once and storing it in the cache, this can help if the * db connection is the slow point. */ public static function build_cache($song_ids) { if (!is_array($song_ids) || !count($song_ids)) { return false; } $idlist = '(' . implode(',', $song_ids) . ')'; // Callers might have passed array(false) because they are dumb if ($idlist == '()') { return false; } // Song data cache $sql = 'SELECT `song`.`id`, `file`, `catalog`, `album`, ' . '`year`, `artist`, `title`, `bitrate`, `rate`, ' . '`mode`, `size`, `time`, `track`, `played`, ' . '`song`.`enabled`, `update_time`, `tag_map`.`tag_id`, '. '`mbid`, `addition_time` ' . 'FROM `song` LEFT JOIN `tag_map` ' . 'ON `tag_map`.`object_id`=`song`.`id` ' . "AND `tag_map`.`object_type`='song' " . "WHERE `song`.`id` IN $idlist"; $db_results = Dba::read($sql); while ($row = Dba::fetch_assoc($db_results)) { parent::add_to_cache('song', $row['id'], $row); $artists[$row['artist']] = $row['artist']; $albums[$row['album']] = $row['album']; if ($row['tag_id']) { $tags[$row['tag_id']] = $row['tag_id']; } } Artist::build_cache($artists); Album::build_cache($albums); Tag::build_cache($tags); Tag::build_map_cache('song',$song_ids); Art::build_cache($albums); // If we're rating this then cache them as well if (Config::get('ratings')) { Rating::build_cache('song', $song_ids); } // Build a cache for the song's extended table $sql = "SELECT * FROM `song_data` WHERE `song_id` IN $idlist"; $db_results = Dba::read($sql); while ($row = Dba::fetch_assoc($db_results)) { parent::add_to_cache('song_data', $row['song_id'], $row); } return true; } // build_cache /** * _get_info */ private function _get_info() { $id = $this->id; if (parent::is_cached('song', $id)) { return parent::get_from_cache('song', $id); } $sql = 'SELECT `id`, `file`, `catalog`, `album`, `year`, `artist`,' . '`title`, `bitrate`, `rate`, `mode`, `size`, `time`, `track`, ' . '`played`, `enabled`, `update_time`, `mbid`, `addition_time` ' . 'FROM `song` WHERE `id` = ?'; $db_results = Dba::read($sql, array($id)); $results = Dba::fetch_assoc($db_results); if (isset($results['id'])) { parent::add_to_cache('song', $id, $results); return $results; } return false; } /** * _get_ext_info * This function gathers information from the song_ext_info table and adds it to the * current object */ public function _get_ext_info() { $id = intval($this->id); if (parent::is_cached('song_data',$id)) { return parent::get_from_cache('song_data',$id); } $sql = "SELECT * FROM song_data WHERE `song_id`='$id'"; $db_results = Dba::read($sql); $results = Dba::fetch_assoc($db_results); parent::add_to_cache('song_data',$id,$results); return $results; } // _get_ext_info /** * fill_ext_info * This calls the _get_ext_info and then sets the correct vars */ public function fill_ext_info() { $info = $this->_get_ext_info(); foreach ($info as $key=>$value) { if ($key != 'song_id') { $this->$key = $value; } } // end foreach } // fill_ext_info /** * type_to_mime * * Returns the mime type for the specified file extension/type */ public static function type_to_mime($type) { // FIXME: This should really be done the other way around. // Store the mime type in the database, and provide a function // to make it a human-friendly type. switch ($type) { case 'spx': case 'ogg': return 'application/ogg'; break; case 'wma': case 'asf': return 'audio/x-ms-wma'; break; case 'mp3': case 'mpeg3': return 'audio/mpeg'; break; case 'rm': case 'ra': return 'audio/x-realaudio'; break; case 'flac'; return 'audio/x-flac'; break; case 'wv': return 'audio/x-wavpack'; break; case 'aac': case 'mp4': case 'm4a': return 'audio/mp4'; break; case 'mpc': return 'audio/x-musepack'; break; default: return 'audio/mpeg'; break; } return true; } /** * get_disabled * * Gets a list of the disabled songs for and returns an array of Songs */ public static function get_disabled($count = 0) { $results = array(); $sql = "SELECT `id` FROM `song` WHERE `enabled`='0'"; if ($count) { $sql .= " LIMIT $count"; } $db_results = Dba::read($sql); while ($r = Dba::fetch_assoc($db_results)) { $results[] = new Song($r['id']); } return $results; } /** * find_duplicates * * This function takes a search type and returns a list of probable * duplicates */ public static function find_duplicates($search_type) { $where_sql = $_REQUEST['search_disabled'] ? '' : "WHERE `enabled` != '0'"; $sql = 'SELECT `id`, `artist`, `album`, `title`, ' . 'COUNT(`title`) FROM `song` ' . $where_sql . ' GROUP BY `title`'; if ($search_type == 'artist_title' || $search_type == 'artist_album_title') { $sql .= ',`artist`'; } if ($search_type == 'artist_album_title') { $sql .= ',`album`'; } $sql .= ' HAVING COUNT(`title`) > 1 ORDER BY `title`'; $db_results = Dba::read($sql); $results = array(); while ($item = Dba::fetch_assoc($db_results)) { $results[] = $item; } // end while return $results; } public static function get_duplicate_info($dupe, $search_type) { $sql = 'SELECT `id` FROM `song` ' . "WHERE `title`='" . Dba::escape($item['title']) . "' "; if ($search_type == 'artist_title' || $search_type == 'artist_album_title') { $sql .= "AND `artist`='" . Dba::escape($item['artist']) . "' "; } if ($search_type == 'artist_album_title') { $sql .= "AND `album` = '" . Dba::escape($item['album']) . "' "; } $sql .= 'ORDER BY `time`,`bitrate`,`size`'; $db_results = Dba::read($sql); $results = array(); while ($item = Dba::fetch_assoc($db_results)) { $results[] = $item['id']; } // end while return $results; } /** * get_album_name * gets the name of $this->album, allows passing of id */ public function get_album_name($album_id=0) { if (!$album_id) { $album_id = $this->album; } $album = new Album($album_id); if ($album->prefix) return $album->prefix . " " . $album->name; else return $album->name; } // get_album_name /** * get_artist_name * gets the name of $this->artist, allows passing of id */ public function get_artist_name($artist_id=0) { if (!$artist_id) { $artist_id = $this->artist; } $artist = new Artist($artist_id); if ($artist->prefix) return $artist->prefix . " " . $artist->name; else return $artist->name; } // get_album_name /** * has_flag * This just returns true or false depending on if this song is flagged for something * We don't care what so we limit the SELECT to 1 */ public function has_flag() { $sql = "SELECT `id` FROM `flagged` WHERE `object_type`='song' AND `object_id`='$this->id' LIMIT 1"; $db_results = Dba::read($sql); if (Dba::fetch_assoc($db_results)) { return true; } return false; } // has_flag /** * set_played * this checks to see if the current object has been played * if not then it sets it to played */ public function set_played() { if ($this->played) { return true; } /* If it hasn't been played, set it! */ self::update_played('1',$this->id); return true; } // set_played /** * compare_song_information * this compares the new ID3 tags of a file against * the ones in the database to see if they have changed * it returns false if nothing has changes, or the true * if they have. Static because it doesn't need this */ public static function compare_song_information($song,$new_song) { // Remove some stuff we don't care about unset($song->catalog,$song->played,$song->enabled,$song->addition_time,$song->update_time,$song->type); $string_array = array('title','comment','lyrics'); $skip_array = array('id','tag_id','mime','mb_artistid','mbid'); // Pull out all the currently set vars $fields = get_object_vars($song); // Foreach them foreach ($fields as $key=>$value) { if (in_array($key,$skip_array)) { continue; } // If it's a stringie thing if (in_array($key,$string_array)) { if (trim(stripslashes($song->$key)) != trim(stripslashes($new_song->$key))) { $array['change'] = true; $array['element'][$key] = 'OLD: ' . $song->$key . ' --> ' . $new_song->$key; } } // in array of stringies else { if ($song->$key != $new_song->$key) { $array['change'] = true; $array['element'][$key] = 'OLD:' . $song->$key . ' --> ' . $new_song->$key; } } // end else } // end foreach if ($array['change']) { debug_event('song-diff', json_encode($array['element']), 5); } return $array; } // compare_song_information /** * update * This takes a key'd array of data does any cleaning it needs to * do and then calls the helper functions as needed. This will also * cause the song to be flagged */ public function update($data) { foreach ($data as $key=>$value) { switch ($key) { case 'artist': // Don't do anything if we've negative one'd this baby if ($value == '-1') { $value = Catalog::check_artist($data['artist_name'], $data['mb_artistid']); } case 'album': if ($value == '-1') { $value = Catalog::check_album($data['album_name'], $data['year'], $data['disk'], $data['mb_albumid']); } case 'title': case 'track': // Check to see if it needs to be updated if ($value != $this->$key) { $function = 'update_' . $key; self::$function($value,$this->id); $this->$key = $value; $updated = 1; } break; default: // Rien a faire break; } // end whitelist } // end foreach // If a field was changed then we need to flag this mofo if ($updated) { Flag::add($this->id,'song','retag','Interface Update'); } return true; } // update /** * update_song * this is the main updater for a song it actually * calls a whole bunch of mini functions to update * each little part of the song... lastly it updates * the "update_time" of the song */ public static function update_song($song_id, $new_song) { $title = Dba::escape($new_song->title); $bitrate = Dba::escape($new_song->bitrate); $rate = Dba::escape($new_song->rate); $mode = Dba::escape($new_song->mode); $size = Dba::escape($new_song->size); $time = Dba::escape($new_song->time); $track = Dba::escape($new_song->track); $mbid = Dba::escape($new_song->mbid); $artist = Dba::escape($new_song->artist); $album = Dba::escape($new_song->album); $year = Dba::escape($new_song->year); $song_id = Dba::escape($song_id); $update_time = time(); $sql = "UPDATE `song` SET `album`='$album', `year`='$year', `artist`='$artist', " . "`title`='$title', `bitrate`='$bitrate', `rate`='$rate', `mode`='$mode', " . "`size`='$size', `time`='$time', `track`='$track', " . "`mbid`='$mbid', " . "`update_time`='$update_time' WHERE `id`='$song_id'"; $db_results = Dba::write($sql); $comment = Dba::escape($new_song->comment); $language = Dba::escape($new_song->language); $lyrics = Dba::escape($new_song->lyrics); $sql = "UPDATE `song_data` SET `lyrics`='$lyrics', `language`='$language', `comment`='$comment' " . "WHERE `song_id`='$song_id'"; $db_results = Dba::write($sql); } // update_song /** * update_year * update the year tag */ public static function update_year($new_year,$song_id) { self::_update_item('year',$new_year,$song_id,'50'); } // update_year /** * update_language * This updates the language tag of the song */ public static function update_language($new_lang,$song_id) { self::_update_ext_item('language',$new_lang,$song_id,'50'); } // update_language /** * update_comment * updates the comment field */ public static function update_comment($new_comment,$song_id) { self::_update_ext_item('comment',$new_comment,$song_id,'50'); } // update_comment /** * update_lyrics * updates the lyrics field */ public static function update_lyrics($new_lyrics,$song_id) { self::_update_ext_item('lyrics',$new_lyrics,$song_id,'50'); } // update_lyrics /** * update_title * updates the title field */ public static function update_title($new_title,$song_id) { self::_update_item('title',$new_title,$song_id,'50'); } // update_title /** * update_bitrate * updates the bitrate field */ public static function update_bitrate($new_bitrate,$song_id) { self::_update_item('bitrate',$new_bitrate,$song_id,'50'); } // update_bitrate /** * update_rate * updates the rate field */ public static function update_rate($new_rate,$song_id) { self::_update_item('rate',$new_rate,$song_id,'50'); } // update_rate /** * update_mode * updates the mode field */ public static function update_mode($new_mode,$song_id) { self::_update_item('mode',$new_mode,$song_id,'50'); } // update_mode /** * update_size * updates the size field */ public static function update_size($new_size,$song_id) { self::_update_item('size',$new_size,$song_id,'50'); } // update_size /** * update_time * updates the time field */ public static function update_time($new_time,$song_id) { self::_update_item('time',$new_time,$song_id,'50'); } // update_time /** * update_track * this updates the track field */ public static function update_track($new_track,$song_id) { self::_update_item('track',$new_track,$song_id,'50'); } // update_track public static function update_mbid($new_mbid,$song_id) { self::_update_item('mbid',$new_mbid,$song_id,'50'); } // update_mbid /** * update_artist * updates the artist field */ public static function update_artist($new_artist,$song_id) { self::_update_item('artist',$new_artist,$song_id,'50'); } // update_artist /** * update_album * updates the album field */ public static function update_album($new_album,$song_id) { self::_update_item('album',$new_album,$song_id,'50'); } // update_album /** * update_utime * sets a new update time */ public static function update_utime($song_id,$time=0) { if (!$time) { $time = time(); } self::_update_item('update_time',$time,$song_id,'75'); } // update_utime /** * update_played * sets the played flag */ public static function update_played($new_played,$song_id) { self::_update_item('played',$new_played,$song_id,'25'); } // update_played /** * update_enabled * sets the enabled flag */ public static function update_enabled($new_enabled,$song_id) { self::_update_item('enabled',$new_enabled,$song_id,'75'); } // update_enabled /** * _update_item * This is a private function that should only be called from within the song class. * It takes a field, value song id and level. first and foremost it checks the level * against $GLOBALS['user'] to make sure they are allowed to update this record * it then updates it and sets $this->{$field} to the new value */ private static function _update_item($field,$value,$song_id,$level) { /* Check them Rights! */ if (!Access::check('interface',$level)) { return false; } /* Can't update to blank */ if (!strlen(trim($value)) && $field != 'comment') { return false; } $value = Dba::escape($value); $sql = "UPDATE `song` SET `$field`='$value' WHERE `id`='$song_id'"; $db_results = Dba::write($sql); return true; } // _update_item /** * _update_ext_item * This updates a song record that is housed in the song_ext_info table * These are items that aren't used normally, and often large/informational only */ private static function _update_ext_item($field,$value,$song_id,$level) { /* Check them rights boy! */ if (!Access::check('interface',$level)) { return false; } $value = Dba::escape($value); $sql = "UPDATE `song_data` SET `$field`='$value' WHERE `song_id`='$song_id'"; $db_results = Dba::write($sql); return true; } // _update_ext_item /** * format * This takes the current song object * and does a ton of formating on it creating f_??? variables on the current * object */ public function format() { $this->fill_ext_info(); // Format the filename preg_match("/^.*\/(.*?)$/",$this->file, $short); $this->f_file = htmlspecialchars($short[1]); // Format the album name $this->f_album_full = $this->get_album_name(); $this->f_album = UI::truncate($this->f_album_full,Config::get('ellipse_threshold_album')); // Format the artist name $this->f_artist_full = $this->get_artist_name(); $this->f_artist = UI::truncate($this->f_artist_full,Config::get('ellipse_threshold_artist')); // Format the title $this->f_title_full = $this->title; $this->f_title = UI::truncate($this->title,Config::get('ellipse_threshold_title')); // Create Links for the different objects $this->link = Config::get('web_path') . "/song.php?action=show_song&song_id=" . $this->id; $this->f_link = "link) . "\" title=\"" . scrub_out($this->title) . "\"> " . scrub_out($this->f_title) . ""; $this->f_album_link = "album . "\" title=\"" . scrub_out($this->f_album_full) . "\"> " . scrub_out($this->f_album) . ""; $this->f_artist_link = "artist . "\" title=\"" . scrub_out($this->f_artist_full) . "\"> " . scrub_out($this->f_artist) . ""; // Format the Bitrate $this->f_bitrate = intval($this->bitrate/1000) . "-" . strtoupper($this->mode); // Format the Time $min = floor($this->time/60); $sec = sprintf("%02d", ($this->time%60) ); $this->f_time = $min . ":" . $sec; // Format the track (there isn't really anything to do here) $this->f_track = $this->track; // Get the top tags $tags = Tag::get_top_tags('song',$this->id); $this->tags = $tags; $this->f_tags = Tag::get_display($tags,$this->id,'song'); // Format the size $this->f_size = UI::format_bytes($this->size); return true; } // format /** * format_pattern * This reformats the song information based on the catalog * rename patterns */ public function format_pattern() { $extension = ltrim(substr($this->file,strlen($this->file)-4,4),"."); $catalog = new Catalog($this->catalog); // If we don't have a rename pattern then just return it if (!trim($catalog->rename_pattern)) { $this->f_pattern = $this->title; $this->f_file = $this->title . '.' . $extension; return; } /* Create the filename that this file should have */ $album = $this->f_album_full; $artist = $this->f_artist_full; $track = sprintf('%02d', $this->track); $title = $this->title; $year = $this->year; /* Start replacing stuff */ $replace_array = array('%a','%A','%t','%T','%y','/','\\'); $content_array = array($artist,$album,$title,$track,$year,'-','-'); $rename_pattern = str_replace($replace_array,$content_array,$catalog->rename_pattern); $rename_pattern = preg_replace("[\-\:\!]","_",$rename_pattern); $this->f_pattern = $rename_pattern; $this->f_file = $rename_pattern . "." . $extension; } // format_pattern /** * get_fields * This returns all of the 'data' fields for this object, we need to filter out some that we don't * want to present to a user, and add some that don't exist directly on the object but are related */ public static function get_fields() { $fields = get_class_vars('Song'); unset($fields['id'],$fields['_transcoded'],$fields['_fake'],$fields['cache_hit'],$fields['mime'],$fields['type']); // Some additional fields $fields['tag'] = true; $fields['catalog'] = true; //FIXME: These are here to keep the ideas, don't want to have to worry about them for now // $fields['rating'] = true; // $fields['recently Played'] = true; return $fields; } // get_fields /** * get_from_path * This returns all of the songs that exist under the specified path */ public static function get_from_path($path) { $path = Dba::escape($path); $sql = "SELECT * FROM `song` WHERE `file` LIKE '$path%'"; $db_results = Dba::read($sql); $songs = array(); while ($row = Dba::fetch_assoc($db_results)) { $songs[] = $row['id']; } return $songs; } // get_from_path /** * @function get_rel_path * @discussion returns the path of the song file stripped of the catalog path * used for mpd playback */ public function get_rel_path($file_path=0,$catalog_id=0) { if (!$file_path) { $info = $this->_get_info(); $file_path = $info->file; } if (!$catalog_id) { $catalog_id = $info->catalog; } $catalog = new Catalog( $catalog_id ); $info = $catalog->_get_info(); $catalog_path = $info->path; $catalog_path = rtrim($catalog_path, "/"); return( str_replace( $catalog_path . "/", "", $file_path ) ); } // get_rel_path /** * play_url * This function takes all the song information and correctly formats a * a stream URL taking into account the downsmapling mojo and everything * else, this is the true function */ public static function play_url($oid) { $song = new Song($oid); $user_id = $GLOBALS['user']->id ? scrub_out($GLOBALS['user']->id) : '-1'; $type = $song->type; // Required for some versions of winamp that won't work if the // stream doesn't end in .ogg This will not break any properly // working player, don't report this as a bug! if ($song->type == 'flac') { $type = 'ogg'; } $song_name = rawurlencode($song->get_artist_name() . " - " . $song->title . "." . $type); $url = Stream::get_base_url() . "type=song&oid=$song->id&uid=$user_id&name=/$song_name"; return $url; } // play_url /** * get_recently_played * This function returns the last X songs that have been played * it uses the popular threshold to figure out how many to pull * it will only return unique object */ public static function get_recently_played($user_id='') { $user_id = Dba::escape($user_id); $sql = "SELECT `object_id`, `user`, `object_type`, `date` " . "FROM `object_count` WHERE `object_type`='song' "; if ($user_id) { $sql .= "AND `user`='$user_id' "; } $sql .= "ORDER BY `date` DESC "; $db_results = Dba::read($sql); $results = array(); while ($row = Dba::fetch_assoc($db_results)) { if (isset($results[$row['object_id']])) { continue; } $results[$row['object_id']] = $row; if (count($results) >= Config::get('popular_threshold')) { break; } } return $results; } // get_recently_played public function get_stream_types() { $types = array(); $transcode = Config::get('transcode_' . $this->type); if ($transcode != 'required') { $types[] = 'native'; } if (make_bool($transcode)) { $types[] = 'transcode'; } return $types; } // end stream_types public function get_transcode_settings($target = null) { $source = $this->type; if ($target) { debug_event('transcode', 'Explicit format request', 5); } else if ($target = Config::get('encode_target_' . $source)) { debug_event('transcode', 'Defaulting to configured target format for ' . $source, 5); } else if ($target = Config::get('encode_target')) { debug_event('transcode', 'Using default target format', 5); } else { $target = $source; debug_event('transcode', 'No default target for ' . $source . ', choosing to resample', 5); } debug_event('transcode', 'Transcoding from ' . $source . ' to ' . $target, 5); $cmd = Config::get('transcode_cmd_' . $source) ?: Config::get('transcode_cmd'); $args = Config::get('encode_args_' . $target); if (!$args) { debug_event('transcode', 'Target format ' . $target . ' is not properly configured', 2); return false; } debug_event('transcode', 'Command: ' . $cmd . ' Arguments: ' . $args, 5); return array('format' => $target, 'command' => $cmd . ' ' . $args); } } // end of song class ?>