/* * kissdx - KiSS PC-Link Daemon eXtended (based on kissd) * * Copyright (C) 2005 Stelian Pop * Portions Copyright (C) 2006 Vidar Tysse * Portions Copyright (C) 2007 Olivier Kahn * * Heavily based on kiss4lin, * Copyright (C) 2004 Jacob Kolding * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. * */ #include #include #include #include #include #include #include #include #include #include #include // Standard GNU Exit code #include #ifdef USE_INTERNAL_SENDFILE #include #else #ifdef Linux #include #endif #endif #include "dvdread.h" #include "kissdx.h" #include "connection.h" #include "config.h" #include "utils.h" #include "sendfile.h" #include "piccache.h" #include "gdstuff/gdstuff.h" #include "backtoback.h" #include "cmdclient.h" #include "cmdserver.h" #define C_PSEUDOFOLDER_PREFIX_FIRSTCHAR "{" #define C_ANY_PSEUDOFOLDER_PREFIX C_PSEUDOFOLDER_PREFIX_FIRSTCHAR"kissdx-" #define C_PSEUDOFOLDER_PREFIX_LASTCHAR "}" #define C_DIR_PSEUDOFOLDER_TYPE "DIR" #define C_ISO_PSEUDOFOLDER_TYPE "ISO" #define C_M3U_PSEUDOFOLDER_TYPE "M3U" #define C_PLS_PSEUDOFOLDER_TYPE "PLS" #define C_B2B_PSEUDOFOLDER_TYPE "B2B" #define C_PSEUDOFOLDER_TYPE_LEN 3 #define C_DIR_PSEUDOFOLDER_PREFIX C_ANY_PSEUDOFOLDER_PREFIX C_DIR_PSEUDOFOLDER_TYPE C_PSEUDOFOLDER_PREFIX_LASTCHAR #define C_ISO_PSEUDOFOLDER_PREFIX C_ANY_PSEUDOFOLDER_PREFIX C_ISO_PSEUDOFOLDER_TYPE C_PSEUDOFOLDER_PREFIX_LASTCHAR #define C_M3U_PSEUDOFOLDER_PREFIX C_ANY_PSEUDOFOLDER_PREFIX C_M3U_PSEUDOFOLDER_TYPE C_PSEUDOFOLDER_PREFIX_LASTCHAR #define C_PLS_PSEUDOFOLDER_PREFIX C_ANY_PSEUDOFOLDER_PREFIX C_PLS_PSEUDOFOLDER_TYPE C_PSEUDOFOLDER_PREFIX_LASTCHAR #define C_B2B_PSEUDOFOLDER_PREFIX C_ANY_PSEUDOFOLDER_PREFIX C_B2B_PSEUDOFOLDER_TYPE C_PSEUDOFOLDER_PREFIX_LASTCHAR #define C_ISO_PSEUDOFILE_SUFFIX ".mpg" #define C_PSEUDODVD_TITLE_PART1 "Title no. " #define C_PSEUDODVD_TITLE_PART2 " of " const char *subtitle_file_extensions = "srt,aqt,sub,rt,smi,ssa,txt"; const char *JPEG_extensions = "jpeg,jpg"; /* Prototypes for external functions */ int handle_playlist(char *path); int CachedSizeResizedJPEG(const char *filename); int CachedSendResizedJPEG(int fd, const char *filename, off_t offset, size_t chunksize); /* Prototypes for internal functions */ static int store_recent(char * filename); static void transform_requested_filename(char *request); /* global variable needs by get_next_picture() and scandir_selector() */ // Evolution may be: Keep track of the scanned directory for a KiSS.PlayerID // to avoid rescanning if the next get request is not moving outside this directory // then keep this variable global and array[playerID] typedef struct get_next_picture_scandir_info_s { char first_picture_basename[PATH_MAX]; // first picture name in browsing directory (loop feature) char last_picture_basename[PATH_MAX]; // last picture name in browsing directory (loop feature) char next_picture_basename[PATH_MAX]; char current_picture_basename[PATH_MAX];// current picture displayed char current_picture_dirname[PATH_MAX]; } get_next_picture_scandir_info_t; static get_next_picture_scandir_info_t get_next_pic_info; short isStoredInHistory = 0; // We store the filename only once in history folder (.recent) // ======================================================================================== // Returns true/false after deciding whether the picture in path is going to be resized int will_resize_jpeg_file(const char *path) { // We resize this file if resizing is configured (width and height) // AND picture scaling has NOT been disabled by the user on the KiSS player // AND this file is in the pictures folder // AND this is not our pseudo-jpeg (show all hidden file toggle) // AND this file has a valid jpeg extension: return (config.picturetargetwidth && config.picturetargetheight && is_image_scaling_enabled && !strncmp(path, config.picturepath, strlen(config.picturepath)) && (!*config.enablehiddenfilestext || strncmp(path + strlen(config.picturepath) + 1, config.enablehiddenfilestext, strlen(config.enablehiddenfilestext))) && list_contains_extension(JPEG_extensions, path)); } /// Get the next picture for possible pre-caching static int get_next_picture(char *next_picture_filename, const char *current_picture_filename) { // CurrentPictureFilename is displayed ==> Try to find the NextPictureFilename (for resizing) // Return 1 to disable a resize cache process on the next picture found // Return 0 for candidate to a resize and precaching process // basename() Library XPG override GNU version and may modify the path by removing trailing '/' characters char filename_buf[PATH_MAX]; // used to split incoming fullname in dirname+basename // Use of global structure "get_next_pic_info_t" *get_next_pic_info.first_picture_basename = '\0'; // first picture name in browsing directory (loop feature) *get_next_pic_info.current_picture_basename ='\0'; *get_next_pic_info.next_picture_basename ='\0'; *get_next_pic_info.last_picture_basename = '\0'; // last picture name in browsing directory (loop feature) struct dirent **namelist; // Recipient for scandir() filtered by scandir_selector() int num_files, n; // Split current constant filename in 2 local variables CurrentBasename and currentDirname strncpy(filename_buf, current_picture_filename,sizeof(filename_buf)); strncpy(get_next_pic_info.current_picture_basename, basename(filename_buf), sizeof(get_next_pic_info.current_picture_basename)); strncpy(get_next_pic_info.current_picture_dirname, dirname(filename_buf), sizeof(get_next_pic_info.current_picture_basename)); // Set global filter to *.* (no restriction on image filename) // Basename - Extension - Dirname - File/Dir mode setup_scandir_selector("*.*", config.picturefileextensions, get_next_pic_info.current_picture_dirname, SCANDIR_FILE_MODE); // scan CurrentPictDirectory, get number of potential other file // use of utils.myFileListCompare():[alpha case insensitive] to define sorting order to entries in *namelist // use local.scandir_selector() : Get only the entries as normal file (no directory accepted) // remove directory entry... and not regular files (socket, block device, fifo named pipe ...) num_files = scandir(get_next_pic_info.current_picture_dirname, &namelist, (void*)scandir_selector, (void*)my_filelist_compare); loglevel(LOGDEBUG,"Debug: GetNxtPic: Scanning directory '%s'",get_next_pic_info.current_picture_dirname); loglevel(LOGDEBUG,"Debug: GetNxtPic: Number of entries scanned (%d)",num_files); if (num_files < 0) { loglevel(LOGERROR,"Error: get_next_picture: scandir '%s': '%s'", get_next_pic_info.current_picture_dirname, strerror(errno)); return -1; } // No files is available in the directory browsed // Standard behavior is to send nothing and KiSS display nothing ==>> we hope end user will understand // Future: Other reaction may be to create a pseudofile "Empty directory" pointing to help screen if (num_files == 0) { // no file in scandir, but Current is not the last one => dir has been cleaned recently ... loglevel(LOGERROR,"Error: get_next_picture: scandir '%s' didn't find next picture for '%s'.", get_next_pic_info.current_picture_dirname, current_picture_filename); return -1; } // Goal: parse namelist[] search (current=name) and populate // *get_next_pic_info.next_picture_basename = namelist[n+1]->d_name); // *get_next_pic_info.first_picture_basename // *get_next_pic_info.last_picture_basename strncpy(get_next_pic_info.first_picture_basename, namelist[0]->d_name, sizeof(get_next_pic_info.first_picture_basename)); strncpy(get_next_pic_info.last_picture_basename, namelist[num_files-1]->d_name, sizeof(get_next_pic_info.last_picture_basename)); for (n = 0; n < num_files; n++) { //No need to search DIRECT type of file, as scandir_selector() has done the filter before //loglevel(LOGDEBUG,"Debug GetNxtPic: loop on namelist n(%d)'%s' vs cur'%s'",n,namelist[n]->d_name, // get_next_pic_info.current_picture_basename); if (!strcmp(namelist[n]->d_name, get_next_pic_info.current_picture_basename)) { if ((n+1) == num_files) { // Last position in directory list // CurrentPic = LastPic ==> Loop on the first picture available strncpy(get_next_pic_info.next_picture_basename, get_next_pic_info.first_picture_basename, sizeof(get_next_pic_info.next_picture_basename)); } else { // current is between first and last picture available strncpy(get_next_pic_info.next_picture_basename, namelist[n+1]->d_name, sizeof(get_next_pic_info.next_picture_basename)); } } free(namelist[n]); } free(namelist); loglevel(LOGDEBUG,"Debug: GetNxtPic: First_pic '%s'",get_next_pic_info.first_picture_basename); loglevel(LOGDEBUG,"Debug: GetNxtPic: Current_pic '%s'",get_next_pic_info.current_picture_basename); loglevel(LOGDEBUG,"Debug: GetNxtPic: Next picture '%s'",get_next_pic_info.next_picture_basename); loglevel(LOGDEBUG,"Debug: GetNxtPic: Last picture '%s'",get_next_pic_info.last_picture_basename); // Condition to be candidate to precached resized picture : //1: First char of config.picturefileextensions must be set // (i.e. config.picturefileextensions must not be an empty string) //2: config.picturefileextensions comma-separated list must contain 'entry->d_name' extension //Fullname is needed for 'will_resize_jpeg_file()' later in this function //snprintf(filename_buf, sizeof get_next_pic_info.current_picture_dirname, "%s/%s", get_next_pic_info.current_picture_dirname, // get_next_pic_info.next_picture_basename); snprintf(next_picture_filename, PATH_MAX, "%s/%s", get_next_pic_info.current_picture_dirname, get_next_pic_info.next_picture_basename); // Final return of next picture found following the current one in alpha non case sensitive order //snprintf(next_picture_filename, PATH_MAX, "%s/%s", // get_next_pic_info.current_picture_dirname, get_next_pic_info.next_picture_basename); loglevel(LOGINFO,"Info: Get_next_picture: NextFound '%s'",next_picture_filename); // Checking the picture found is candidate for a resizing/cache ==> return 0 (else return 1) // File starting with '.' (hidden) are also candidate to precache // and directory (. & ..) has been removed in scandir_selector() // Next picture is compliant with PictureExtension defined in kissdx.conf if (config.picturefileextensions && *config.picturefileextensions && list_contains_extension(config.picturefileextensions, get_next_pic_info.next_picture_basename) && will_resize_jpeg_file(next_picture_filename) ) { // this next picture found needs to be resized and precached loglevel(LOGDEBUG, "Debug: GetNxtPic: Candidate to precache '%s'", get_next_pic_info.next_picture_basename); return 0; } else { loglevel(LOGDEBUG, "Debug: GetNxtPic: Not candidate to precache '%s'", get_next_pic_info.next_picture_basename); return 1; } // for security only loglevel(LOGDEBUG,"Debug: GetNxtPic: Not candidate to precache '%s'",get_next_pic_info.next_picture_basename); return 1; } // ******************************************************************************* static void precache_next_picture(const char *current_picture_filename) { // We fork a child process for this and then return immediately to continue processing // NOTE: This child will be orphaned (since we are already a child process) and init will take care of it pid_t precache_child_pid; // child pid pid_t precache_daemon_pid; // daemon pid precache_daemon_pid = getpid(); precache_child_pid = fork(); switch (precache_child_pid) { case -1: // Fork error wile attempting to fork loglevel(LOGERROR,"Error: precache_next_picture: fork failed: %s", strerror(errno)); return; case 0: // Child execution { // We must not return from here, we must exit // if return, the calling logic will continue in this child process, in parallel with the parent process char next_picture_filename[PATH_MAX] = "", cached_filename[PATH_MAX]; if (piccache_is_precaching_in_progress()) _exit(0); // We can only have one pre-caching process running loglevel(LOGDEBUG,"Debug: precache_next_picture: Search next picture of '%s'",current_picture_filename); if (get_next_picture(next_picture_filename, current_picture_filename)) _exit(0); // There is no next picture - nothing for us to do loglevel(LOGDEBUG,"Debug: precache_next_picture: Extract sizing info of '%s'",next_picture_filename); if (!piccache_get_entry_filename(cached_filename, next_picture_filename, config.picturetargetwidth, config.picturetargetheight, config.picturemaxzoompercent)) _exit(0); // The next picture is already cached loglevel(LOGDEBUG,"Debug: precache_next_picture: Precache resized jpeg of '%s'",next_picture_filename); if (!piccache_set_precaching_status(next_picture_filename)) { loglevel(LOGINFO,"Info: Pre-caching %s", next_picture_filename); CachedSizeResizedJPEG(next_picture_filename); piccache_clear_precaching_status(); } _exit(0); } default: // Parent execution loglevel(LOGINFO,"Info: Precaching trial fork child [%d]",precache_child_pid); return; } } /// Types and vars for pseudofile commands et al enum pseudofile_command_t {pfc_none=0, pfc_list_hidden_entries, pfc_image_scaling}; static enum pseudofile_command_t pseudofile_command = pfc_none; static void * commandpseudofile_data = NULL; static int commandpseudofile_size = 0; // Pseudo file: PictureRoot : Feature Image Scaling (On/Off) static char * image_scaling_gui_text = "Toggle image scaling"; //static char * image_scaling_gui_enable_txt = "Feature: Enable Picture scaling"; //static char * image_scaling_gui_disable_txt = "Feature: Disable Picture scaling"; /// If path is a "Command Pseudofile", generate the confirmation jpeg in memory and return true. static int create_commandpseudofile_if_needed(const char* path) { if (!strncmp(path, config.picturepath, strlen(config.picturepath))) { char text[255]; const char *filename = path + strlen(config.picturepath) + 1; if (*config.enablehiddenfilestext && !strncmp(filename, config.enablehiddenfilestext, strlen(config.enablehiddenfilestext))) { if (!commandpseudofile_data) { static const char* header = "CONFIGURATION CHANGE"; static const char* footer = "Press exit and re-enter listing to use this setting."; static const char* text1 = "Viewing of hidden content"; snprintf(text, sizeof text, "is now %s", config.listhiddenentries? "disabled" : "enabled"); if (!config.listhiddenentries && config.enablehiddenfilesminutes) snprintf(text + strlen(text), sizeof text - strlen(text), " for %d minute%s", config.enablehiddenfilesminutes, config.enablehiddenfilesminutes > 1? "s" : ""); strncat(text, ".", sizeof text - strlen(text)); if (!(commandpseudofile_data = create_text_jpeg(header, text1, text, footer, &commandpseudofile_size)) || commandpseudofile_size <= 0) log("create_commandpseudofile_if_needed: create_text_jpeg returned NULL or size %d.", commandpseudofile_size); } pseudofile_command = pfc_list_hidden_entries; return 1; } else if (!strncmp(filename, image_scaling_gui_text, strlen(image_scaling_gui_text))) { if (!commandpseudofile_data) { static const char* header = "BEHAVIOUR CHANGE"; static const char* footer = "Please return to the file list to use this setting."; static const char* text1 = "Scaling of pictures"; snprintf(text, sizeof text, "is now %s", is_image_scaling_enabled? "disabled" : "enabled"); strncat(text, ".", sizeof text - strlen(text)); if (!(commandpseudofile_data = create_text_jpeg(header, text1, text, footer, &commandpseudofile_size)) || commandpseudofile_size <= 0) log("create_commandpseudofile_if_needed: create_text_jpeg returned NULL or size %d.", commandpseudofile_size); } pseudofile_command = pfc_image_scaling; return 1; } // Process any other Action Pseudofiles here in "else if" blocks } return 0; } // ======================================================== /// Send a local command to our main process /// Perform appropriate command when user selects a Command Pseudofile static void perform_pseudofile_command(void) { char command[LOCAL_COMMAND_SIZE]; switch (pseudofile_command) { case pfc_list_hidden_entries: { // Compose a local command that will toggle the value of config.listhiddenentries in our main process sprintf(command, "SET LIST_HIDDEN_ENTRIES %s", config.listhiddenentries? "OFF" : "ON"); deliver_local_command(command); break; } case pfc_image_scaling: { // Compose a local command that will toggle the value of is_image_scaling_active in our main process sprintf(command, "SET IMAGE_SCALING %s", is_image_scaling_enabled? "OFF" : "ON"); deliver_local_command(command); break; } case pfc_none: log0("No pseudofile command to be performed (none)"); break; default: log0("No pseudofile command to be performed (default)"); break; } } // ======================================================== /// Check if there is a "kissdx.override" file in the media directory of the supplied media file path. /// If override file found, then parse it and set overridden values into config and config_override. /// Optimization: Only read override file once per process (use flag to indicate if already read). static void check_for_config_overrides(const char *mediaFilePath) { static int hasCheckedAlready = 0; if (!hasCheckedAlready) { const char *pBasename = strrchr(mediaFilePath, '/'); if (pBasename) { const char *configOverrideFilename = "kissdx.override"; char filename[PATH_MAX]; int dirLen = pBasename - mediaFilePath + 1; if (dirLen + strlen(configOverrideFilename) < sizeof(filename)) { struct stat statbuf; int stat_rc; strncpy(filename, mediaFilePath, dirLen); *(filename + dirLen) = '\0'; strcat(filename, configOverrideFilename); stat_rc = stat(filename, &statbuf); if (stat_rc < 0 && errno != ENOENT) { loglevel(LOGERROR,"Error check_for_config_overrides: Couldn't stat '%s': %s", filename, strerror(errno)); } else { if (stat_rc == 0) { // Parse the override file and apply changes to current config (in this subprocess only) config_settings_t new_config; memcpy(&new_config, &config, sizeof (config)); int config_error = parse_config(&new_config, filename); if (!config_error) { free_config(&config); memcpy(&config, &new_config, sizeof (config)); } else { loglevel(LOGERROR,"Error: Loading config override failed from: %s", filename); } } } } } hasCheckedAlready = 1; // Set flag so we don't do this again in this process } } /// Transform a subtitle filename according to the subtitlefilemapping configuration option static void transform_subtitle_filename(char *filename) { // Change filename provided based on options.subtitle_catchall_pattern = {name}*.* // Response to KiSS player 7 requested subtitle file, a set of available one // Even if player request a .sub file, we send the first subtitle file found (alphanumeric sort) // ex: consider exiting: movie.avi movie.sub and movie.srt and movie.txt // Here the table of corresponding file sent back to the player // request0: Get movie.avi Send movie.avi [200] and SIZE // request1: Get movie.srt Send movie.srt [200] and SIZE // request2: Get movie.aqt Send movie.sub [200] and SIZE // request3: Get movie.sub Send movie.txt [200] and SIZE // request4: Get movie.rt Send [404] no more subtitle found // request5: Get movie.smi Send [404] no more subtitle found // request6: Get movie.ssa Send [404] no more subtitle found // request7: Get movie.txt Send [404] no more subtitle found loglevel(LOGINFO,"Info \n\ntransform_subtitle_filename: Starting %s",filename ); char saveFilenameProvided[PATH_MAX]; // save incoming filename in case of premature return of error //char tmpSubtitleFound[PATH_MAX]; // store subtitle full path and name found char tmpVideoPath[PATH_MAX]; char tmpDirname[PATH_MAX]; // tmp variable as dirname() modify/return data in input char tmpSubstituteBuf[PATH_MAX]; // tmp variable to manipulate data before substitute // variable used in conjunction with global scandir_xxxx char scandir_Subtitle_Pathname[PATH_MAX]; // pathname resulting of subtitute to trigger the scandir char scandir_Subtitle_Basename_Pattern[PATH_MAX]; char *ptmpSubstitutePath; // Dirname after substitution of pattern element char *p; char extRequested[4]; // extension requested by KiSS player, then we know that size is limited to 4 int ext_index, isRecent, n,num_files; struct stat statbuf; // used to read symlink content and retrieve original movie file struct dirent **subtitle_namelist; strncpy(saveFilenameProvided, filename, sizeof(saveFilenameProvided)); saveFilenameProvided[sizeof(saveFilenameProvided) - 1] = '\0'; //(0) First, do nothing if "mapping for subtitle" option is not defined if (!*config.subtitle_catchall_pattern) {// subtitle_catchall_pattern != {name}*.* loglevel(LOGINFO,"Info transform_subtitle_filename: No mapping definition %s","" ); return; // Subtitle filename mapping has not been configured } //(A) Find input file's index into the list of subtitle filename extensions. // This will be used to pick the appropriate subtitle file if (!(p = strrchr(filename, '.'))) { // if not '.' then no extension is found (error) loglevel(LOGERROR,"Error transform_subtitle_filename: No extension found: %s:%s",filename,strerror(errno)); return; } p++; // p identify the extension of incoming filename strncpy(extRequested,p, sizeof extRequested); extRequested[3]='\0'; if (!*p || !(p = (char *)list_pointer(subtitle_file_extensions, extRequested, ",", lsmt_exact))) { loglevel(LOGERROR,"Error transform_subtitle_filename: Undefined extension found: %s:%s",filename,strerror(errno)); return; // Unknown extension } for (ext_index = 0; p > subtitle_file_extensions; p--) { if (*p == ',') { ext_index++; } } // refering : srt,aqt,sub,rt,smi,ssa,tx loglevel(LOGINFO,"Info transform_subtitle_filename: extension '%s' index[0-6]: %d",extRequested,ext_index); //(B) - Check a history folder/file exit for videopath // Retrieve the actual fullname of avi movie if the request is a history call (aka recent feature) /* If the requested movie is coming from the history pathname (.recent): * please notice that only the movie file name is stored in history path (through a symbolic link) * then the subtitle requested by KiSS player (ex: ./.recent/video/movie.srt) * should first be transformed to retrieve original path of the movie related (movie.avi) (and only .avi !) * and second search the appropriate subtitle available (ex: /media/video/movie.srt) * * Get movie history dirname from option repository (get_recent_path(video)) => char *tmpVideoPath[PATH_MAX] * Check with input => flag used is : isRecent * If recent file is requested: (only movie is kept in history (not subtitle) then use movie extension (workaround) * change filename extension with .avi to find its symbolic link (basename non case sensitive function) * >get in return the actual existing fullname: char *recentVideoFullname[PATH_MAX] * try to open the link and get the real fullfilename * end with original movie.avi path in "tmpVideoPath" */ if (get_recent_path(tmpVideoPath, config.videopath)) { // get_recent return a strncpy in input parameter (tmpVideoPath) (do not modify the char pointer) loglevel(LOGERROR,"Error transform_subtitle_filename: videopath undefined: %s:%s",tmpVideoPath,strerror(errno)); return; } // carefull: dirname modifies path (in removing trailing basename) (do not use original string data) strncpy(tmpDirname,filename,sizeof tmpDirname); tmpDirname[sizeof(tmpDirname) - 1] = '\0'; strncpy(tmpDirname, dirname(tmpDirname), sizeof tmpDirname); isRecent = !strcmp(tmpDirname, tmpVideoPath); // check recent if (isRecent) { p= strrchr(filename, '.') + 1; // extension is no more needed because ext_index keep track of the rank willing for subtitle strcpy(p, "avi"); // original film should exist in history folder (that is < PATH_MAX) //*(p+3)='\0'; // securely close the string in case of input filename with extension less than 3 char loglevel(LOGINFO,"Info transform_subtitle_filename: filename.avi %d:%s",strlen(filename),filename); // carefull: casestat() modify the provided filename with the one actually found // if nothing is found, filename becomes: path/.avi (not so good to continue with) strncpy(tmpVideoPath,filename,sizeof (tmpVideoPath)); tmpVideoPath[sizeof(tmpVideoPath) - 1] = '\0'; loglevel(LOGINFO,">Info transform_subtitle_filename: tmpVideoPath.avi %d:%s",strlen(tmpVideoPath),tmpVideoPath); if (casestat(tmpVideoPath, &statbuf) < 0) { loglevel(LOGERROR,"Error transform_subtitle_filename: video, linked with subtitle, is unreachable: %s:%s",filename,strerror(errno)); // restaure filename before quiting to enable normal behavior after quiting this function strncpy(filename,saveFilenameProvided,PATH_MAX); filename[PATH_MAX-1] = '\0'; return; } loglevel(LOGINFO,"Info transform_subtitle_filename: after casestat tmpVideoPath.avi: %s",tmpVideoPath); // Read content of symblink named [filename].avi and store it in tmpVideoPath if ((n = readlink(filename, tmpVideoPath, PATH_MAX)) > 0) { *(tmpVideoPath + n) = '\0'; loglevel(LOGINFO,"Info transform_subtitle_filename: video real filename:%d '%s'",n,tmpVideoPath); } else { loglevel(LOGERROR,"Error transform_subtitle_filename: Cannot find real file for symlink '%s':%s",filename,strerror(errno)); return; } } //(C) /* C-Generate the pattern to scan directory and isolate the file according ext_index rank * Store in global varaible : char *basename_pattern[PATH_MAX] = option value (coming from kissdx.conf) * */ // Replace the special tokens in the pattern with our substitutes for this file strncpy(scandir_Subtitle_Basename_Pattern, config.subtitle_catchall_pattern,sizeof scandir_Subtitle_Basename_Pattern); scandir_Subtitle_Basename_Pattern[sizeof(scandir_Subtitle_Basename_Pattern) - 1] = '\0'; loglevel(LOGINFO,"Info transform_subtitle_filename: pattern:%d '%s'", strlen(scandir_Subtitle_Basename_Pattern),scandir_Subtitle_Basename_Pattern); // Find original EXT and replace it strncpy(tmpSubstituteBuf,saveFilenameProvided,sizeof tmpSubstituteBuf); tmpSubstituteBuf[sizeof(tmpSubstituteBuf) - 1] = '\0'; p = strrchr(tmpSubstituteBuf, '.') + 1; // char *p; pointe to EXT requested string_substitute(scandir_Subtitle_Basename_Pattern, "{ext}" , p ,sizeof scandir_Subtitle_Basename_Pattern); loglevel(LOGINFO,"Info transform_subtitle_filename: post replace EXT(%s) : %s",p,scandir_Subtitle_Basename_Pattern); // Find original NAME and replace it *(p-1) ='\0'; // end filename before EXT p = basename(tmpSubstituteBuf); string_substitute(scandir_Subtitle_Basename_Pattern, "{name}", p, sizeof scandir_Subtitle_Basename_Pattern); loglevel(LOGINFO,"Info transform_subtitle_filename: post replace NAME(%s) : %s",p, scandir_Subtitle_Basename_Pattern); // scandirSubtitlePathname // if {path} in defined pattern => if recent, get 'tmpVideoPath' else 'tmpDirname' if (isRecent) { ptmpSubstitutePath = dirname(tmpVideoPath); } else { ptmpSubstitutePath = tmpDirname; } //isPathInPattern string_substitute(scandir_Subtitle_Basename_Pattern, "{path}", ptmpSubstitutePath, sizeof scandir_Subtitle_Basename_Pattern); loglevel(LOGINFO,"Info transform_subtitle_filename: post replace PATH : %s",scandir_Subtitle_Basename_Pattern); // Finally, isolate path and basename strncpy(tmpSubstituteBuf,scandir_Subtitle_Basename_Pattern,sizeof tmpSubstituteBuf); tmpSubstituteBuf[sizeof(tmpSubstituteBuf) - 1] = '\0'; strncpy(scandir_Subtitle_Basename_Pattern , basename(scandir_Subtitle_Basename_Pattern),sizeof scandir_Subtitle_Basename_Pattern); scandir_Subtitle_Basename_Pattern[sizeof(scandir_Subtitle_Basename_Pattern) - 1] = '\0'; strncpy(scandir_Subtitle_Pathname , dirname(tmpSubstituteBuf),sizeof scandir_Subtitle_Pathname); scandir_Subtitle_Pathname[sizeof(scandir_Subtitle_Pathname) - 1] = '\0'; // If no {path} identifier is used in scandir_pattern (from kissdx.conf) => use ptmpSubstitutePath if (!strcmp(scandir_Subtitle_Pathname,".")) { loglevel(LOGINFO,"Info transform_subtitle_filename: default {path} scandir_Subtitle_Pathname: %s",ptmpSubstitutePath); strncpy(scandir_Subtitle_Pathname , ptmpSubstitutePath, sizeof scandir_Subtitle_Pathname); scandir_Subtitle_Pathname[sizeof(scandir_Subtitle_Pathname) - 1] = '\0'; } loglevel(LOGINFO,"Info transform_subtitle_filename: Finaly Pathname: %d:%s", strlen(scandir_Subtitle_Pathname),scandir_Subtitle_Pathname); loglevel(LOGINFO,"Info transform_subtitle_filename: Finaly Filename: %d:%s", strlen(scandir_Subtitle_Basename_Pattern),scandir_Subtitle_Basename_Pattern); //strncpy(tmpSubtitleFound, scandir_Subtitle_Pathname ,sizeof(tmpSubtitleFound)); // tmpSubtitleFound[sizeof(tmpSubtitleFound) - 1] = '\0'; //(D) Scandir to get subtitle according a pattern // Scandir needs both dirname(filename) and scandir_selector(respect pattern,extension and not directory) // scandir function is based on global variable for the scandir_selector filter /* char scandir_basename_pattern[PATH_MAX] = ""; // ex: movie*.* * char scandir_file_extensions[PATH_MAX] = ""; // ex: srt,apt,txt,sub... * char scandir_dirname[PATH_MAX] = ""; // ex: /data/media/video/Childrenchar * short scandir_directory_limited ; // flag 1=Only directory name is retrieved by scandir() */ setup_scandir_selector(scandir_Subtitle_Basename_Pattern, subtitle_file_extensions, scandir_Subtitle_Pathname, SCANDIR_FILE_MODE); num_files = scandir(scandir_Subtitle_Pathname, &subtitle_namelist, (void*)scandir_selector, (void*)my_filelist_compare); loglevel(LOGINFO,"Info transform_subtitle_filename: scandir numfiles(%d): with pattern '%s'",num_files, scandir_Subtitle_Basename_Pattern); if (num_files < 0) { loglevel(LOGERROR,"Error transform_subtitle_filename: scandir %s: %s", scandir_Subtitle_Pathname, strerror(errno)); // restaure filename before quiting to enable normal behavior after quiting this function strncpy(filename,saveFilenameProvided,PATH_MAX); filename[PATH_MAX-1] = '\0'; loglevel(LOGERROR,"Error transform_subtitle_filename: restaure filename %d:%s",strlen(filename),filename); return; } //(E) Scandir to get subtitle according a pattern /* > loop on result * (info) reject result without subtitle extension (already done with the selector) * (info) reject result not matching the pattern (already done with the selector) * Once on the record according requested rank is isolated * Concat scandir_dirname and this result entry (name&ext) * free all * if out of loop range, then substitue NameExt with "Empty.txt" * >> return a filename modified * */ // If less file found than extension index => give back the original requested filename // num_files[0, 1-x] ext_index[0-6] if ((!num_files) || (num_files < (ext_index+1))) { // Research with pattern does not find any subtitle corresponding strncpy(filename, ptmpSubstitutePath ,PATH_MAX); strcat(filename,"/"); filename[PATH_MAX-1] = '\0'; p= basename(saveFilenameProvided); strcat(filename,p); loglevel(LOGINFO,"INFO no matching ext_index(%d) to subtitle rank found(%d): use original path '%s'", ext_index+1,num_files,p,filename); } else { // Research with pattern found subtitles for (n = 0; n < num_files; n++) { loglevel(LOGDEBUG,"Info for loop numfiles(%d) namelist(%d)='%s'",num_files, n,subtitle_namelist[n]->d_name); if (n == ext_index) { strncpy(filename, scandir_info.scandir_dirname ,PATH_MAX); strcat(filename,"/"); filename[PATH_MAX-1] = '\0'; strcat(filename, subtitle_namelist[n]->d_name); loglevel(LOGINFO,"INFO (n==ext_index)(%d) filename '%s'",n,filename); } free(subtitle_namelist[n]); // More than 7 subtitle filename is not supported => information message to log (only once) if ((num_files > 6) && (n == 6)) { loglevel(LOGINFO,"Info: KiSS player supports up to 7 subtitles. Current pattern %s is found %d times", scandir_Subtitle_Basename_Pattern, num_files); } } } free(subtitle_namelist); loglevel(LOGINFO,"Info transform_subtitle_filename: quit with filename %d:%s",strlen(filename),filename); return; } /* * Name: timesort * * Description: Sort function for scandir which sorts by time. * Most recent files first. */ static int timesort(const void * f1, const void * f2) { char fname1[PATH_MAX]; char fname2[PATH_MAX]; char cwd[PATH_MAX]; struct stat st1, st2; struct dirent *d1; struct dirent *d2; if (f1 == NULL || f2 == NULL) return 0; d1 = *(struct dirent **)f1; d2 = *(struct dirent **)f2; if (getcwd(cwd, PATH_MAX) == NULL) return 0; snprintf(fname1, PATH_MAX, "%s/%s", cwd, d1->d_name); snprintf(fname2, PATH_MAX, "%s/%s", cwd, d2->d_name); if (lstat(fname1, &st1) < 0) loglevel(LOGERROR,"Error Couldn't stat '%s'", fname1); if (lstat(fname2, &st2) < 0) loglevel(LOGERROR,"Error Couldn't stat '%s'", fname2); if (st1.st_ctime > st2.st_ctime) return -1; /* if timestamp is bigger, file is newer */ if (st1.st_ctime < st2.st_ctime) return 1; /* if timestamp is smaller, file is older */ return 0; } static int run_trigger(char *command, char *filename, size_t fsize, char *result, size_t size) { int pid, status; int p[2]; size_t len; if (!command[0]) { if (result) memcpy(result, filename, min(fsize, size)); return 0; } logv("Running trigger '%s' on file '%s'", command, filename); if (pipe(p) < 0) { log("trigger pipe() failed: %s", strerror(errno)); return -1; } pid = fork(); if (pid < 0) { log("trigger fork() failed: %s", strerror(errno)); close(p[0]); close(p[1]); return -1; } if (pid == 0) { setpgid(pid, 0); close(p[0]); close(STDOUT_FILENO); if (dup2(p[1], STDOUT_FILENO) < 0) { log("trigger dup2() failed: %s", strerror(errno)); _exit(1); } execlp(command, command, filename, NULL); log("trigger exec() failed: %s", strerror(errno)); _exit(1); } close(p[1]); if (waitpid(pid, &status, 0) < 0) { log("trigger waitpid() failed: %s", strerror(errno)); close(p[0]); return -1; } if (!WIFEXITED(status)) { log("trigger child exited abnormally: %x", status); close(p[0]); return -1; } if (WEXITSTATUS(status)) { log("trigger child returned non 0: %d", WEXITSTATUS(status)); close(p[0]); return -1; } if (result) { if ((len = read(p[0], result, size)) < 0) { log("trigger read() failed: %s", strerror(errno)); close(p[0]); return -1; } while (len > 0 && (result[len - 1] == '\r' || result[len - 1] == '\n')) len--; result[len < size ? len : len - 1] = '\0'; } close(p[0]); return 0; } // The network socket used to communicate with the player. // Only a few functions should use this, all others call those functions. static int player_socket = -1; /// Receive a single line from the player (line terminated by LF, CRLF or CR) /// NOTE: The line terminators have been stripped from the returned string /// NOTE: We do character set conversion from player to server filenames here! static int receive_line_from_player(char *buffer, int size, int timeout_seconds) { int len = receive_line(player_socket, buffer, size, timeout_seconds); if (len < 0) return len; if (len > 0 && can_convert_charset(cept_client, cept_server)) { // NOTE: We convert the entire incoming line, when we really should only convert filenames strcpy(buffer, convert_charset(buffer, cept_client, cept_server)); len = strlen(buffer); } logv("<-- [%s]", buffer); return len; } /// Send a character buffer of the given size to the player. /// NOTE: We do NOT do any character set conversion! static int send_bytes_to_player(const char *buffer, int size) { if (size > 1 || *buffer != '\n') // Don't log a single LF (we want a pretty log :-)) loglevel(LOGPROTO,"Proto: Send --> [%s]", buffer); return send_bytes(player_socket, buffer, size); } /// Send a character string plus a linefeed to the player. /// NOTE: We do character set conversion from server filenames to player here! static void send_line_to_player(const char *line) { // NOTE: We convert the entire outgoing line, when we really should only convert filenames const char* line_p = can_convert_charset(cept_server, cept_client)? convert_charset(line, cept_server, cept_client) : line; int len = strlen(line_p); if (send_bytes_to_player(line_p, len) == len) if (send_bytes_to_player("\n", 1) < 0) loglevel(LOGERROR,"Error: Send linefeed: %s", strerror(errno)); } #define OUTGOING_LINE_SIZE (2 * PATH_MAX) #define FILE_SEQNO_DIGITS 4 /// Remove a sequence number at the supplied string position. static void remove_seqno_at_path_position(char *pathpart_p) { char *firstspace_p = pathpart_p + FILE_SEQNO_DIGITS; // We expect to find a space char here int pathpart_len = strlen(pathpart_p); if (pathpart_len < FILE_SEQNO_DIGITS + 2) return; // Unexpected: No room for a sequence number in this filename for (const char *p = pathpart_p; p < firstspace_p ; p++) if (!strchr("0123456789", *p)) return; // Unexpected: This is not a sequence number if (*firstspace_p != ' ') return; // Unexpected: There is not a space char after the sequence number // Remove the sequence number memmove(pathpart_p, firstspace_p + 1, pathpart_len - FILE_SEQNO_DIGITS); } /// Remove the sequence number from the start of the filename part and each directory part of /// the supplied path if displaysequencenumbers = yes in the config file and we see such a number. static void remove_seqno_from_incoming_listpath(char *path) { if (config.displaysequencenumbers) { for (char *slash_p = strchr(path, '/'); slash_p != NULL; slash_p = strchr(slash_p + 1, '/')) { remove_seqno_at_path_position(slash_p + 1); } if (*path != '/') remove_seqno_at_path_position(path); } } /// Remove the sequence number from the start of the filename in the supplied path /// if displaysequencenumbers = yes in the config file and we see such a number. static void remove_seqno_from_incoming_filename(char *path) { if (config.displaysequencenumbers) { char *lastslash_p = strrchr(path, '/'); char *filename_p = lastslash_p? lastslash_p + 1 : path; remove_seqno_at_path_position(filename_p); } } /// Add a sequence number at the start of the filename in the supplied outgoing line /// if displaysequencenumbers = yes in the config file. /// Return true if we actually added a sequence number and populated linewithseqno, false if we did not. /// Requirements: /// - linewithseqno must be at least OUTGOING_LINE_SIZE characters in size. static int add_seqno_to_outgoing_list_line(const char *line, char *linewithseqno) { static int seqno_counter = 0; static char file_seqno_format[32] = ""; if (config.displaysequencenumbers) { int line_len = strlen(line); if (line_len + FILE_SEQNO_DIGITS + 1 < OUTGOING_LINE_SIZE) { // Test for available space in linewithseqno buffer const char *title_end_p = strchr(line, '|'); const char *filename_end_p = title_end_p? strchr(title_end_p + 1, '|') : NULL; if (title_end_p && filename_end_p) { // Construct the seqence number format for snprintf (only done first time we're called) if (!*file_seqno_format) snprintf(file_seqno_format, sizeof(file_seqno_format), "%%0%dd ", FILE_SEQNO_DIGITS); // We must place the sequence no. before the filename, after the path if any // Determine the length of the leading part of the line, before the sequence no. int part1_len = (title_end_p - line) + 1; // Assuming no path in filename for (const char *lastslash_p = filename_end_p - 1; lastslash_p > title_end_p; lastslash_p--) if (*lastslash_p == '/') { part1_len = (lastslash_p - line) + 1; break; } int part2_len = line_len - part1_len; // Start building the modified line. strncpy(linewithseqno, line, part1_len); // Produce the seqence number in this line, and a trailing '\0' character snprintf(linewithseqno + part1_len, OUTGOING_LINE_SIZE - part1_len - part2_len, file_seqno_format, ++seqno_counter); // Append the rest of the line to complete the modified line strcat(linewithseqno, line + part1_len); return 1; } } } return 0; } /// Send a line of a LIST response to the player after /// 1. adding a sequence number at the start of the filename (optional), /// 2. converting the line from server character set to client character set and /// 3. appending a linefeed at the end of the line. void send_list_line_to_player(const char* line) { // TODO: Use conditional malloc for outgoing_line to save memory when config.displaysequencenumbers = no? static char outgoing_line[OUTGOING_LINE_SIZE]; if (add_seqno_to_outgoing_list_line(line, outgoing_line)) send_line_to_player(outgoing_line); else send_line_to_player(line); } /// Open a pseudo-folder as a DVD static dvdreader_t open_DVD_cached(char *foldername) { char realFullName[PATH_MAX]; // Will store the real foldername for a DVD folder, or the real filename for an ISO file. This is the name that we will send to libdvdread. int len; // We expect, and can only handle, a request for a path/filename in one of the following formats: // [/path]/ // [/path]/ char basepath[PATH_MAX] = {'\0'}, pseudoFolderType[16] = {'\0'}, real_filename[PATH_MAX] = {'\0'}; const char *formatString = "%[^"C_PSEUDOFOLDER_PREFIX_FIRSTCHAR"]"C_ANY_PSEUDOFOLDER_PREFIX"%[^"C_PSEUDOFOLDER_PREFIX_LASTCHAR"]"C_PSEUDOFOLDER_PREFIX_LASTCHAR"%[^|]"; int assignedCount = sscanf(foldername, formatString, &basepath, &pseudoFolderType, &real_filename); if (assignedCount != 3) { log("invalid request for pseudo-folder. Assigned %d, expected 3. Path/name must be like \"%s\", but was: \"%s\"", assignedCount, formatString, foldername); return NULL; }; if (strcasecmp(pseudoFolderType, C_ISO_PSEUDOFOLDER_TYPE) && strcasecmp(pseudoFolderType, C_DIR_PSEUDOFOLDER_TYPE)) { log("invalid request for pseudo-folder, folder type must be "C_ISO_PSEUDOFOLDER_TYPE" or "C_DIR_PSEUDOFOLDER_TYPE", but was: \"%s\"", pseudoFolderType); return NULL; }; // Construct the real fully qualified filename if ((len = snprintf(realFullName, sizeof(realFullName), "%s%s", basepath, real_filename)) < sizeof(realFullName)) { // Open the DVD image / folder dvdreader_t dvd = CachedDvdOpen(realFullName, 1); if (!dvd) log("open_DVD_cached: CachedDvdOpen failed for %s, title %d", realFullName, 1); return dvd; // Normal return } else log("Buffer too small: snprintf returned %d, expected < %d for open_DVD_cached(\"%s\")", len, sizeof(realFullName), foldername); return NULL; // Error return } /// Open a pseudo-file (a DVD title) as a DVD media file static dvdreader_t open_DVD_title_cached(char *foldername) { char realFullName[PATH_MAX]; // Will store the real foldername for a DVD folder, or the real filename for an ISO file. This is the name that we will send to libdvdread. int len; // We expect, and can only handle, a request for a path/filename in one of the following formats: // [/path]//Title no. nn of nn // [/path]//Title no. nn of nn char basepath[PATH_MAX], pseudoFolderType[16], real_filename[PATH_MAX]; int titleNo, titleCount; const char *formatString = "%[^"C_PSEUDOFOLDER_PREFIX_FIRSTCHAR"]"C_ANY_PSEUDOFOLDER_PREFIX"%[^"C_PSEUDOFOLDER_PREFIX_LASTCHAR"]"C_PSEUDOFOLDER_PREFIX_LASTCHAR"%[^/]/"C_PSEUDODVD_TITLE_PART1"%d"C_PSEUDODVD_TITLE_PART2"%d"C_ISO_PSEUDOFILE_SUFFIX; int assignedCount = sscanf(foldername, formatString, &basepath, &pseudoFolderType, &real_filename, &titleNo, &titleCount); if (assignedCount != 5) { log("invalid request for pseudo-file title. Assigned %d, expected 5. Path/name must be like \"%s\", but was: \"%s\"", assignedCount, formatString, foldername); return NULL; }; if (strcasecmp(pseudoFolderType, C_ISO_PSEUDOFOLDER_TYPE) && strcasecmp(pseudoFolderType, C_DIR_PSEUDOFOLDER_TYPE)) { log("invalid request for pseudo-file title. Folder type must be "C_ISO_PSEUDOFOLDER_TYPE" or "C_DIR_PSEUDOFOLDER_TYPE", but was: \"%s\"", pseudoFolderType); return NULL; }; // Construct the real fully qualified filename if ((len = snprintf(realFullName, sizeof(realFullName), "%s%s", basepath, real_filename)) < sizeof(realFullName)) { // Open the DVD image / folder dvdreader_t dvd = CachedDvdOpen(realFullName, titleNo); if (!dvd) log("open_DVD_title_cached: CachedDvdOpen failed for %s, title %d", realFullName, titleNo); return dvd; // Normal return } else log("Buffer too small: snprintf returned %d, expected < %d for open_DVD_title_cached(\"%s\")", len, sizeof(realFullName), foldername); return NULL; // Error return } /// Send a list of all titles in the DVD or playlist that a pseudo-folder represents static void list_pseudofolder(const char *base, const char *path) { dvdreader_t dvd; char fullpath[PATH_MAX], tpath[PATH_MAX], line[OUTGOING_LINE_SIZE]; int pseudoFolderPrefixLen = strlen(C_ANY_PSEUDOFOLDER_PREFIX); const char *pFileName = strrchr(path, '/'); pFileName = pFileName? pFileName + 1 : path; if (!strncmp(pFileName, C_ANY_PSEUDOFOLDER_PREFIX, pseudoFolderPrefixLen)) { int len; if ((len = snprintf(fullpath, PATH_MAX, "%s/%s", base, path)) < PATH_MAX) { clean_pathname(fullpath); if (verify_path(fullpath) < 0) { log("access denied: %s", fullpath); goto done; } if (run_trigger(config.directorypretrigger, fullpath, strlen(fullpath) + 1, tpath, sizeof(tpath)) < 0) { log("directorypretrigger %s failed", tpath); return; } if (!strncmp(pFileName + pseudoFolderPrefixLen, C_ISO_PSEUDOFOLDER_TYPE, C_PSEUDOFOLDER_TYPE_LEN) || !strncmp(pFileName + pseudoFolderPrefixLen, C_DIR_PSEUDOFOLDER_TYPE, C_PSEUDOFOLDER_TYPE_LEN)) { // *** List a DVD folder *** if ((dvd = open_DVD_cached(tpath))) { int titleCount = CachedDvdGetTitleCount(); for (int i = 0; i < titleCount; i++) { // Send this title as a filename to the player char titleName[PATH_MAX], fpath[PATH_MAX]; snprintf(titleName, sizeof titleName, "%s%02d%s%02d%s", C_PSEUDODVD_TITLE_PART1, i+1, C_PSEUDODVD_TITLE_PART2, titleCount, C_ISO_PSEUDOFILE_SUFFIX); if ((len = snprintf(fpath, sizeof fpath, "%s/%s", tpath, titleName)) < sizeof fpath) { if ((len = snprintf(line, sizeof line, "%s|%s|0|", titleName, fpath)) < sizeof line) send_list_line_to_player(line); else log("Buffer too small: snprintf to fpath in list_pseudofolder() returned %d, expected < %d for titlename \"%s\"", len, sizeof line, titleName); } else log("Buffer too small: snprintf in list_pseudofolder() returned %d, expected < %d for titlename \"%s\"", len, sizeof fpath, titleName); } } } else if (!strncmp(pFileName + pseudoFolderPrefixLen, C_M3U_PSEUDOFOLDER_TYPE, C_PSEUDOFOLDER_TYPE_LEN) || !strncmp(pFileName + pseudoFolderPrefixLen, C_PLS_PSEUDOFOLDER_TYPE, C_PSEUDOFOLDER_TYPE_LEN)) { // *** List a PLAYLIST folder *** char realPlaylistPath[PATH_MAX]; strcpy(realPlaylistPath, tpath); char *pLastSlash = strrchr(realPlaylistPath, '/'); char *pRealPlaylistFileName = pLastSlash + 1 + strlen(C_M3U_PSEUDOFOLDER_PREFIX); memmove(pLastSlash + 1, pRealPlaylistFileName, strlen(pRealPlaylistFileName) + 1); handle_playlist(realPlaylistPath); } else log("Internal error: list_pseudofolder() inexplicably wrecked pseudofolder filename: %s", pFileName); run_trigger(config.directoryposttrigger, tpath, sizeof(tpath), NULL, 0); } else log("Buffer too small: snprintf in list_pseudofolder() returned %d, expected < %d for path \"%s\"", len, PATH_MAX, path); } else log("Internal error: list_pseudofolder() was called with non-pseudofolder argument: %s", path); done: send_line_to_player("EOL"); } // ====================================================================================== static void list_folder(const char *base, const char *path, enum mediastore_t mediastore) { /* base = Root path of a mediastore (ex: /home/media/video) (e.g: comes from config.audiopath) * path = Media path defined in the request [LIST xxx|path| (first time, strlen(path)=0 path="") * path is relative path related to media store root * In the opposite, LIST command send back a full pathname to afford a GET FullPath player request * mediastore in {mst_audio, mst_video, mst_picture} * * ================================================================================== * Output syntax: [displayed_name|local_media_path|0 or 1|] : 0=media 1=directory * =========== Output sample ======================================================== * [10609] 2007-10-30 13:15:08 <-- [LIST VIDEO ||] * [ 7905] 2007-10-30 13:15:08 kissdx fork child for services [10609] * [10609] 2007-10-30 13:15:09 --> [[Recently played]|[Recently played]|1|] * [10609] 2007-10-30 13:15:09 --> [{kissdx-DIR}MyDVD_image|{kissdx-DIR}MyDVD_image|1|] * [10609] 2007-10-30 13:15:09 --> [other_dir|other_dir|1|] * [10609] 2007-10-30 13:15:09 --> [dir|dir|1|] * [10609] 2007-10-30 13:15:09 --> [/Christmas.avi|/home/myuser/media/video/Christmas.avi|0|] * [10609] 2007-10-30 13:15:09 --> [EOL] * * */ char fullpath[PATH_MAX], tpath[PATH_MAX], line[OUTGOING_LINE_SIZE], *thisname; int n, to_send, len; int mp3_count = 0, ogg_count = 0; int is_recent_directory = 0; struct dirent **namelist; char fpath[PATH_MAX]; struct stat statbuf; const char *validExtensions = mediastore == mst_audio? config.audiofileextensions : mediastore == mst_video? config.videofileextensions : mediastore == mst_picture? config.picturefileextensions : NULL; const mediapathlist_t *extraPathList = mediastore == mst_audio? &config.extra_audiopath_list : mediastore == mst_video? &config.extra_videopath_list : mediastore == mst_picture? &config.extra_picturepath_list : NULL; if (!extraPathList) { log("Internal error: Unknown mediastore number %d", (int)mediastore); } int (*sortmethod)(const void *,const void *) = my_filelist_compare; *fullpath = '\0'; // Get filename (removed from the full path)(last word preceded by /, or fullname if no / exit) const char *pFileName = strrchr(path, '/'); pFileName = pFileName? pFileName + 1 : path; //-- Special processing if this is a pseudo-path if (!strncmp(pFileName, C_ANY_PSEUDOFOLDER_PREFIX, strlen(C_ANY_PSEUDOFOLDER_PREFIX))) { const char *use_base = base, *use_path = path; if (extraPathList) { const char *firstslash_p; for (int i = 0; i < extraPathList->count; i++) { if (strstr(path, extraPathList->list[i]->name) == path) { if (*(firstslash_p = path + strlen(extraPathList->list[i]->name)) == '\0' || *firstslash_p == '/') { // This is a request for a pseudo-folder inside an "extra folder" use_base = extraPathList->list[i]->path; use_path = firstslash_p; break; } } } } list_pseudofolder(use_base, use_path); return; } //-- Set fullpath = the real path that is to be listed if (config.max_recent_files && !strcmp(path, config.recentlyusedfoldername)) { // This is a request for the [Recently used] folder if (get_recent_path(fullpath, base)) goto done; // Error! sortmethod = timesort; is_recent_directory = 1; } else if (extraPathList) { const char *firstslash_p; for (int i = 0; i < extraPathList->count; i++) { if (strstr(path, extraPathList->list[i]->name) == path) { if (*(firstslash_p = path + strlen(extraPathList->list[i]->name)) == '\0' || *firstslash_p == '/') { // This is a request for (an entry inside) an "extra folder" strcpy(fullpath, extraPathList->list[i]->path); strcat(fullpath, firstslash_p); break; } } } } if (*fullpath == '\0') { // This is a normal request to list a real folder if ((len = snprintf(fullpath, sizeof fullpath, "%s/%s", base, path)) >= sizeof fullpath) { log("Buffer too small: snprintf returned %d, expected < %d for path \"%s\"", len, sizeof fullpath, path); goto done; } } //-- Verify that the path can be listed clean_pathname(fullpath); if (verify_path(fullpath) < 0) { log("access denied: %s", fullpath); goto done; } // == Run trigger and send back the fullpath to be displayed in "tpath" if (run_trigger(config.directorypretrigger, fullpath, strlen(fullpath) + 1, tpath, sizeof(tpath)) < 0) { log("directorypretrigger %s failed", tpath); return; } chdir(tpath); /* scandir does not provide path info, so set it */ if ((n = scandir(tpath, &namelist, (void*)NULL, (void*)sortmethod)) < 0) { log("scandir %s: %s", tpath, strerror(errno)); goto done; } /* ** HISTORY DIRECTORY MANAGEMENT (aka RecentlyUsed folder) * Output: [[Recently played]|[Recently played]|1|] * =============================================================== * RecentlyUsed feature prerequisite to be displayed : * -A name to be displayed: option 'recentlyusedfoldername' may redefine the default value =[Recently used] * -A path to store data : * a) option 'persistentstoragepath' is set outside of media repositories * b) default value is creating .recent folder at root of each media (./video ./audio ./picture) * -A max number of history: option 'max_recent_files' * -A current 'list media' request that is at the root of the repository * get list /media/video == [LIST VIDEO ||] ==> Does generate a RecentlyUsed entry * get list /media/video/test == [[LIST VIDEO ||/test] ==> Does not generate RecentlyUsed entry * * (root directory is tested with (*path==\0)) * * Special case : .recent in media root repository => ListHiddenEntry test is neede : * if ListHiddenEntry=Yes ==> .recent and RecentlyUsed will create a duplicate entry * if ListHiddenEntry=No ==> only RecentlyUsed will be displayed (no duplicate) * * if .recent(persistent) is set inside media repository ==> * if .recent == media.root ==> detected in config.c and rejected (kissdx not start) * if .recent == media.anySubdirectory ==> undetected and kissdx runs * ==> to be studied more * * Test: If no PersistentStorage path and no ListHidden => RecentlyUsed folder is displayed * Test: If no PersistentStorage path and ListHidden yes => RecentlyUsed folder is NOT displayed */ if (config.max_recent_files && !*path && (!config.listhiddenentries || *config.persistentstoragepath)) { if ((to_send = snprintf(line, sizeof line, "%s|%s|1|", config.recentlyusedfoldername, config.recentlyusedfoldername)) < sizeof line) send_list_line_to_player(line); else loglevel(LOGERROR,"Error List_folder: History path too long (%d), max (%d) for '%s'", to_send, sizeof line, config.recentlyusedfoldername); } /* End of HISTORY FOLDER Mgt */ /* ** MULTI MEDIASTORE DEFINITION MANAGEMENT * If multiple media store in defined in .conf "multiple line of {audio, picture,video}path" * The first mediastore found in .conf is deplayed with a content browsing * The other ones are only refered as entries (like any subdirectory could be) * This feature only occurs for a "LIST xxx||" (root request (*path is '\0')): * and when more than one media folder has been configured for this media store */ if (!*path && extraPathList) { for (int i = 0; i < extraPathList->count; i++) { const char *extraFolderName = extraPathList->list[i]->name; if ((to_send = snprintf(line, sizeof line, "%s|%s|1|", extraFolderName, extraFolderName)) < sizeof line) send_list_line_to_player(line); else loglevel(LOGERROR,"Error List_folder: Multi mediastore path too long (%d), max (%d) for '%s'", to_send, sizeof line, extraFolderName); } } /* End of MULTI MEDIASTORE FOLDER Mgt */ // First, send directory names (including pseudo-folders) for (int i = 0; i < n; i++) { int list_this_entry = -1; thisname = namelist[i]->d_name; if (!config.listhiddenentries) { // Don't list hidden directories unless specified by option if (*thisname == '.') list_this_entry = 0; } else { // Never list the "this level" and "previous level" pseudo directory entries if (!strcmp(thisname, ".") || !strcmp(thisname, "..")) list_this_entry = 0; } if (list_this_entry) { if ((len = snprintf(fpath, sizeof fpath, "%s/%s", tpath, thisname)) < sizeof fpath) { clean_pathname(fpath); // DEBUG unreachable fs mounted (usb unplugged) loglevel(LOGDEBUG,"DDDDDD before stat :%s",fpath); /* int ffd=open(fpath, O_RDONLY| O_NONBLOCK); int save_file_flags = fcntl(ffd, F_GETFL); save_file_flags |= O_NONBLOCK; fcntl(ffd, F_SETFL,save_file_flags); char buff[128]; int rc=read(ffd,buff,sizeof buff); loglevel(LOGDEBUG,"DDDDDD Read (%d) '%s'",rc,buff); if (rc < 0) { // -1 if not open success loglevel(LOGDEBUG,"DDDDDD FAILED TO OPEN %s","O_NONBLOCK"); } else { loglevel(LOGDEBUG,"DDDDDD S§UCCESS TO OPEN rc=%d in %s",ffd,"O_NONBLOCK"); } */ // Get the filetype so we can test whether this is a directory mode_t entry_type = 0; int entry_exists; if ( (entry_exists = (stat(fpath, &statbuf) == 0)) ) { //if ( (entry_exists = (fstat(ffd, &statbuf) == 0)) ) { //loglevel(LOGDEBUG,"DDDDDD after stat :%s",fpath); entry_type = statbuf.st_mode & S_IFMT; } else { loglevel(LOGWARN, "Warn: stat of '%s': %s", fpath, strerror(errno)); } // DEBUG unreachable fs unmounted : close(ffd); // Set up a pointer to the filename extension, if there is one char *ext = strrchr(thisname, '.'); if (ext) ext++; // *** Substitute a pseudo-folder for each audio playlist file *** // We add our special prefix so we will recognize the folder when the player asks for it later int isPlayablePlaylistFile = ( entry_type != S_IFDIR && mediastore == mst_audio && ext && (!strcasecmp(ext, "m3u") || !strcasecmp(ext, "pls")) && construct_pseudo_name(!strcasecmp(ext, "M3U")? C_M3U_PSEUDOFOLDER_PREFIX : C_PLS_PSEUDOFOLDER_PREFIX, &thisname, strlen(tpath))); // *** Substitute a pseudo-folder for each playable ISO file *** // We add our special prefix so we will recognize the folder when the player asks for it later int isPlayableIsoFile = ( entry_type != S_IFDIR && mediastore == mst_video && ext && list_contains_extension(config.isofileextensions, thisname) && construct_pseudo_name(C_ISO_PSEUDOFOLDER_PREFIX, &thisname, strlen(tpath))); // *** Substitute a pseudo-folder for any directory that is a DVD root *** // We add our special prefix so we will recognize the folder when the player asks for it later if (entry_exists && entry_type == S_IFDIR && mediastore == mst_video) { char statpath[PATH_MAX], statdir[PATH_MAX]; if ((len = snprintf(statdir, sizeof statdir, "%s/VIDEO_TS", fpath)) < sizeof statdir) { if (casestat(statdir, &statbuf) == 0 && S_ISDIR(statbuf.st_mode)) { if ((len = snprintf(statpath, sizeof statpath, "%s/VIDEO_TS.IFO", statdir)) < sizeof statpath) { if (casestat(statpath, &statbuf) == 0 && !S_ISDIR(statbuf.st_mode)) { if ((len = snprintf(statpath, sizeof statpath, "%s/VIDEO_TS.BUP", statdir)) < sizeof statpath) { if (casestat(statpath, &statbuf) == 0 && !S_ISDIR(statbuf.st_mode)) { // That's good enough. We assume that this is a DVD root folder construct_pseudo_name(C_DIR_PSEUDOFOLDER_PREFIX, &thisname, strlen(tpath)); } } else log("Buffer too small: snprintf returned %d, expected < %d for statpath 2 of path \"%s\"", len, sizeof statpath, statdir); } } else log("Buffer too small: snprintf returned %d, expected < %d for statpath 1 of path \"%s\"", len, sizeof statpath, statdir); } } else log("Buffer too small: snprintf returned %d, expected < %d for statdir of path \"%s\"", len, sizeof statdir, fpath); } if (entry_type == S_IFDIR || isPlayableIsoFile || isPlayablePlaylistFile) { // Send this directory name to the player if ((to_send = snprintf(line, sizeof line, "%s|%s|1|", thisname, thisname)) < sizeof line) send_list_line_to_player(line); else log("Buffer too small: snprintf returned %d, expected < %d for sending thisname \"%s\"", to_send, sizeof line, thisname); // Mark this entry as processed (the filename loop below will skip these) *(namelist[i]->d_name) = '\0'; } else { if (mediastore == mst_audio && !is_recent_directory && (len = strlen(thisname)) > 4) { const char* p = thisname + len - 4; if (!strcasecmp(p, ".mp3")) mp3_count++; else if (!strcasecmp(p, ".ogg")) ogg_count++; } } } else log("Buffer too small: snprintf returned %d, expected < %d for thisname \"%s\"", len, sizeof fpath, thisname); } } // Show a pseudo-file.mp3 to be selected for playing all mp3 files in this directory back-to-back as a single large file. // Do this only in Audio area, only if there are at least two mp3 files in this directory, and never in the .recent directory if (mp3_count > 1) { char* pDirname = strrchr(tpath, '/'); if (pDirname && strlen(pDirname) > 0) thisname = pDirname + 1; else thisname = "Play all MP3 files back-to-back"; if (construct_pseudo_name(C_B2B_PSEUDOFOLDER_PREFIX, &thisname, strlen(tpath))) { if ((to_send = snprintf(line, sizeof line, "%s.mp3|%s/%s.mp3|0|", thisname, tpath, thisname)) < sizeof line) send_list_line_to_player(line); else log("Buffer too small: snprintf returned %d, expected < %d for mp3 back-to-back \"%s\"", to_send, sizeof line, thisname); } } // Show a pseudo-file.ogg to be selected for playing all ogg files in this directory back-to-back as a single large file. // Do this only in Audio area, only if there are at least two ogg files in this directory, and never in the .recent directory if (ogg_count > 1) { char* pDirname = strrchr(tpath, '/'); if (pDirname && strlen(pDirname) > 0) thisname = pDirname + 1; else thisname = "Play all OGG files back-to-back"; if (construct_pseudo_name(C_B2B_PSEUDOFOLDER_PREFIX, &thisname, strlen(tpath))) { if ((to_send = snprintf(line, sizeof line, "%s.ogg|%s/%s.ogg|0|", thisname, tpath, thisname)) < sizeof line) send_list_line_to_player(line); else log("Buffer too small: snprintf returned %d, expected < %d for ogg back-to-back \"%s\"", to_send, sizeof line, thisname); } } // Next, send the file names for (int i = 0; i < n; i++) { int list_this_entry = -1; thisname = namelist[i]->d_name; // Don't list hidden files unless specified by option if (!config.listhiddenentries && *thisname == '.') list_this_entry = 0; // Don't process the directories; they were processed in the loop above if (*thisname == '\0') list_this_entry = 0; // Only list files with extensions that match the configuration for this media store (no config means list all files) if (list_this_entry && validExtensions && *validExtensions) if (!list_contains_extension(validExtensions, thisname)) list_this_entry = 0; if (list_this_entry) { // This file name should be sent to the player if ((len = snprintf(fpath, sizeof fpath, "%s/%s", tpath, thisname)) < sizeof fpath) { clean_pathname(fpath); // Change the filename extension if it has an extension that is to be renamed // and there is no real file with the same name and the target extension. rename_filetype_outgoing(fpath); // Send this file name to the player pFileName = strrchr(fpath, '/'); pFileName = pFileName? pFileName + 1 : fpath; if ((to_send = snprintf(line, sizeof line, "%s|%s|0|", pFileName, fpath)) < sizeof line) send_list_line_to_player(line); else log("Buffer too small: snprintf returned %d, expected < %d for sending file \"%s\" (2)", to_send, sizeof line, pFileName); } else log("Buffer too small: snprintf returned %d, expected < %d for sending file \"%s\" (1)", len, sizeof fpath, thisname); } free(namelist[i]); } free(namelist); // Show a pseudo-jpeg to be selected to toggle scaling of pictures. // Do this only in Picture root. if (!*path && mediastore == mst_picture) { to_send = snprintf(line, sizeof line, "%s.jpg|%s/%s.jpg|0|",image_scaling_gui_text, // First releaes display only on filename : image_scaling_gui_text, //is_image_scaling_enabled? image_scaling_gui_disable_txt : image_scaling_gui_enable_txt, config.picturepath, image_scaling_gui_text); if (to_send < sizeof line) send_list_line_to_player(line); else log("Buffer too small: snprintf returned %d, expected < %d for image_scaling_gui_text \"%s\"", to_send, sizeof line, image_scaling_gui_text); } // Show a pseudo-jpeg to be selected to toggle hidden content visible/invisible. // Do this only in Picture root and only if enablehiddenfilestext is configured if (*config.enablehiddenfilestext && !*path && mediastore == mst_picture) { to_send = snprintf(line, sizeof line, "%s.jpg|%s/%s.jpg|0|", config.enablehiddenfilestext, config.picturepath, config.enablehiddenfilestext); if (to_send < sizeof line) send_list_line_to_player(line); else log("Buffer too small: snprintf returned %d, expected < %d for enablehiddenfilestext \"%s\"", to_send, sizeof line, config.enablehiddenfilestext); } run_trigger(config.directoryposttrigger, tpath, sizeof(tpath), NULL, 0); done: send_line_to_player("EOL"); } // **************************************************************************** static void handle_list(char *request, size_t reqsize) { char *p1, *p2; p1 = strchr(request, '|'); p2 = strrchr(request, '|'); if (p1 == NULL || p2 == NULL) { log("invalid LIST command \"%s\"", request); return; } *p2 = '\0'; char *path = p1 + 1; // Remove the sequence number that is added in all the outgoing folder names when // config setting displaysequencenumbers = yes. remove_seqno_from_incoming_listpath(path); // Handle the LIST request if (!strncmp(request + 5, "AUDIO", 5)) list_folder(config.audiopath, path, mst_audio); else if (!strncmp(request + 5, "VIDEO", 5)) list_folder(config.videopath, path, mst_video); else if (!strncmp(request + 5, "PICTURE", 7)) list_folder(config.picturepath, path, mst_picture); else log("unknown LIST command \"%s\"", request); } // ***************************************************************************** int is_b2b_filename(const char* filename) { const char *pFileName = strrchr(filename, '/'); pFileName = pFileName? pFileName + 1 : filename; return !strncmp(pFileName, C_B2B_PSEUDOFOLDER_PREFIX, strlen(C_B2B_PSEUDOFOLDER_PREFIX)); } // ****************************************************************************** /// For back-to-back pseudofile: Send the size of the back-to-back combined file /// For DVD pseudofile: Send the size of the DVD title that a pseudo-file represents static void handle_size_pseudofile(char *request, size_t reqsize) { char response[20]; char *p1, *filename; u_int64_t size; p1 = strchr(request, '|'); if (p1 == NULL) { loglevel(LOGERROR,"Error: invalid SIZE command for pseudo-file: '%s'", request); return; } *p1 = '\0'; filename = request + 5; if (verify_path(filename) < 0) { loglevel(LOGERROR,"access denied for pseudo-file: '%s'", filename); return; } if (is_b2b_filename(filename)) { // Find the size of the entire combined back-to-back concatenated file if (!CachedB2bOpen(filename)) loglevel(LOGERROR,"CachedB2bOpen failed for '%s'", filename); size = CachedB2bGetSize(); } else { // Find the size of the entire DVD movie dvdreader_t dvd; if (!(dvd = open_DVD_title_cached(filename))) return; size = CachedDvdGetSize(); } snprintf(response, sizeof(response), "%015lld", (long long)size); send_bytes_to_player(response, 15); } // *************************************************************************** /// For back-to-back pseudofile: Send the requested chunk of media content from the back-to-back combined file /// For DVD pseudofile: Send the requested chunk of media content from the DVD title that a pseudo-file represents static void handle_get_pseudofile(char *request, size_t reqsize, dvdreader_t dvd) { char *filename; char *p1, *p2; long long o; long c; off_t offset; size_t chunk; ssize_t sent; loglevel(LOGINFO,"Info fct:Handle_Get_Pseudofile '%s'",request); p1 = strchr(request, '/'); p2 = strchr(request, '|'); if (p1 == NULL || p2 == NULL) { loglevel(LOGERROR,"Error: invalid GET command for pseudo-file: \"%s\"", request); return; } *p2 = '\0'; filename = p1; if (sscanf(p2 + 1, "%lld %ld", &o, &c) != 2) { loglevel(LOGERROR,"Error: invalid GET parameters for pseudo-file: \"%s\"", p2 + 1); return; }; offset = o; chunk = c; int was_open = 0; if (is_b2b_filename(filename)) { // Send data from combined back-to-back file was_open = CachedB2bIsOpen(filename); if (!CachedB2bOpen(filename)) loglevel(LOGERROR,"Error: CachedB2bOpen failed for %s", filename); if ((sent = CachedB2bSendChunk(player_socket, offset, chunk)) < 0) { loglevel(LOGERROR,"Error: handle_get_pseudofile: CachedB2bSendChunk failed,%s",""); } } else { // Send data from DVD was_open = (dvd != NULL); if (!dvd) { if (verify_path(filename) < 0) { log("access denied for pseudo-file: %s", filename); return; } if (!(dvd = open_DVD_title_cached(filename))) return; } if ((sent = CachedDvdSendChunk(player_socket, offset, chunk)) < 0) { log0("handle_get_pseudofile: CachedDvdSendChunk failed"); } } if (sent == -1) sent = 0; // Clear error indicator value, indicate bytes sent loglevel(LOGPROTO,"Proto: Send --> [%d bytes at offset %lld]", sent, offset); /* The client asked for more data than we have, send padding bytes */ if (sent < chunk) logv("--> [%d padding null bytes]", chunk - sent); while (sent++ < chunk) write(player_socket, "\0", 1); if (!was_open && config.max_recent_files && !isStoredInHistory) { store_recent(filename); isStoredInHistory = 1; // We store the filename only once in history folder (.recent) } } // *********************************************************************************** static void handle_action1_pseudofile(char *request, size_t reqsize) { /// Send 200 if the requested DVD title or back-to-back combined file can be opened, send 404 otherwise. /// Then enter a tight loop that will serve the DVD contents to the player. loglevel(LOGINFO,"Info fct:Handle_Action1_Pseudofile '%s'",request); char *p1, *p2; dvdreader_t dvd = NULL; int len; p1 = strchr(request, '/'); p2 = strrchr(request, '|'); if (p1 == NULL || p2 == NULL) { log("invalid ACTION command for pseudo-file: \"%s\"", request); return; } *p2 = '\0'; char *path = p1; int is_b2b = is_b2b_filename(path);loglevel(LOGDEBUG,"Debug: is b2b(%d)",is_b2b); if (is_b2b) { if (!CachedB2bOpen(path)) { send_bytes_to_player("404", 3); return; } } else { if (!(dvd = open_DVD_title_cached(path))) { send_bytes_to_player("404", 3); return; } } if (send_bytes_to_player("200", 3) < 0) { if (is_b2b) CachedB2bClose(); else CachedDvdClose(); return; } if (config.max_recent_files && !isStoredInHistory) { store_recent(path); isStoredInHistory = 1; } while (1) { static char newreq[512]; if ((len = receive_line_from_player(newreq, sizeof(newreq), config.networktimeoutinterval)) < 0) { if (is_b2b) CachedB2bClose(); else CachedDvdClose(); return; } transform_requested_filename(newreq); if (!strncmp(newreq, "SIZE", 4)) handle_size_pseudofile(newreq, 512); else if (!strncmp(newreq, "GET", 3)) handle_get_pseudofile(newreq, 512, dvd); else loglevel(LOGERROR,"Error: unknown ACTION command for pseudo-file: '%s'", newreq); } } // ************************************************************************* static void handle_size(char *request, size_t reqsize) { struct stat st; char response[20]; char *p1; char tfilename[PATH_MAX]; long long filesize; p1 = strchr(request, '|'); if (p1 == NULL) { loglevel(LOGERROR,"Error: handle_size invalid command '%s'", request); return; } else { loglevel(LOGPROTO,"Proto:PcLink request:[%s]",request); } *p1 = '\0'; char *path = request + 5; int isSubtitleFile = list_contains_extension(subtitle_file_extensions, path); if (isSubtitleFile) transform_subtitle_filename(path); int isActionPseuodFile = create_commandpseudofile_if_needed(path); if (isActionPseuodFile && commandpseudofile_data) { filesize = (long long)commandpseudofile_size; snprintf(response, sizeof(response), "%015lld", filesize); send_bytes_to_player(response, 15); return; } if (verify_path(path) < 0) { loglevel(LOGERROR,"Error: handle_size access denied: '%s'", path); return; } if (run_trigger(config.pretrigger, path, reqsize - 5, tfilename, sizeof(tfilename)) < 0) { loglevel(LOGERROR,"Error: handle_size pretrigger failed: '%s'", tfilename); return; } if (stat(tfilename, &st) < 0) { loglevel(LOGERROR,"Error: handle_size stat '%s': %s", tfilename, strerror(errno)); return; } if (will_resize_jpeg_file(tfilename)) { if (config.picturecachesize && config.picturecachesize != 1) { loglevel(LOGDEBUG,"Debug: handle_size piccache_wait :'%s'",tfilename); piccache_wait_if_precaching_this(tfilename); } loglevel(LOGDEBUG,"Debug: handle_size get size of cached file :'%s'",tfilename); filesize = (long long)CachedSizeResizedJPEG(tfilename); } else { check_for_config_overrides(path); filesize = config_overrides.bogusfilesize > 0 ? config_overrides.bogusfilesize : // Send bogus file size to player (long long)st.st_size; // Send real file size to player } snprintf(response, sizeof(response), "%015lld", filesize); send_bytes_to_player(response, 15); run_trigger(config.posttrigger, tfilename, sizeof(tfilename), NULL, 0); } // ************************************************************************ static void handle_get(char *request, size_t reqsize, int fd) { /* GET /home/olivier/media/picture/Paris/other02/IMG_5632.JPG| 8192 4096 |oqmzybpxqdu1| * Notice that KiSS player send a multiple GET MEDIA request to achieve a complete download of media * Once per 4Kbytes: first call [0..4096], second call [4096..8192] and so on * Optimisation of exchange between player and kissdx: * - is to be discussed * - * * Parameter 'fd' is a file descriptor on the media requested (movie, picture, audio) * Coming from a ACTION1 request, fd is linked to the media (simple response 200/404 is expected) * Coming from a GET request, fd is not defined (-1) */ char *filename; // filename part of the request //char *p1, *p2, *p3, *p4, *p5; char *pRequestFile, *pRequestOffset,*pRequestPlayerId; char *pRequestPseudoFolder1, *pRequestPseudoFolder2 ; off_t offset, requested_offset; size_t chunk; int lfd = -1; long long requestOffset; long requestChunk ; // oftently chunk is 32768 char (size of each data request) char tfilename[PATH_MAX]; char playerid[32]; #if defined(USE_INTERNAL_SENDFILE) || defined(Linux) ssize_t sent; #else /* FreeBSD */ soff_t sent; #endif pRequestFile = strchr(request, '/'); // filename part of request pRequestOffset = strchr(request, '|'); // offset part of request pRequestPlayerId = strrchr(pRequestOffset, '|'); // playerid part of request if (pRequestFile == NULL || pRequestOffset == NULL || pRequestPlayerId == NULL) { loglevel(LOGERROR,"Error: handle_get Invalid command '%s'", request); return; } else { loglevel(LOGPROTO,"Proto:PcLink request:[%s]",request); } strncpy(playerid,pRequestPlayerId,sizeof playerid); playerid[strlen(playerid)-2]='\0'; //remove trailing '|' Should be used for admin module (status) //loglevel(LOGINFO,"Info: handle_get request from playerid '%s'",playerid); // Special handling of pseudo files for playing ISO images / VIDEO_TS folders and audio playlists // see those exemple: // === Music stream : // ACTION 2 /home/olivier/media/musique/Idir/{kissdx-B2B}Idir.mp3| // GET /home/olivier/media/musique/Idir/{kissdx-B2B}Idir.mp3| 124955372 127 |oqmzybpxqdu1| // pRequestPseudoFolderBegin refer "/Idir" ==> no pseudo folder transformation // === ISO image stream : // ACTION 2 /home/olivier/media/video/{kissdx-DIR}HommeOrchestre/Title no. 01 of 01.mpg| // GET /home/olivier/media/video/{kissdx-DIR}HommeOrchestre/Title no. 01 of 01.mpg| 0 16384 |oqmzybpxqdu1| // pRequestPseudoFolderBegin refer "/{kissdx-DIR}HommeOrchestre" ==> Perform pseudofolder transformation // ?? is it the case of image inside a iso file ? pRequestPseudoFolder1 = strrchr(request, '/'); // Search end of pseudo directory name requested if (pRequestPseudoFolder1) { *pRequestPseudoFolder1 = '\0'; pRequestPseudoFolder2 = strrchr(request, '/'); //loglevel(LOGDEBUG,"Debug: pRequestPseudoFolder1 '%s' ",pRequestPseudoFolder1+1); //loglevel(LOGDEBUG,"Debug: pRequestPseudoFolder2 '%s' ",pRequestPseudoFolder2); // Check starting string "{kissdx-" in pRequestPseudoFolderBegin and replace corresponding folder if ( (pRequestPseudoFolder2 && strncmp(pRequestPseudoFolder2 +1, C_ANY_PSEUDOFOLDER_PREFIX, strlen(C_ANY_PSEUDOFOLDER_PREFIX)) == 0)) { //Only last directory name is checked as a "pseudo" information *pRequestPseudoFolder1 = '/'; // We temporarily had a '\0' here if (fd == -1) { // If nothing to read on the TCP stream (via fd) handle_get_pseudofile(request, reqsize, NULL); } else { loglevel(LOGERROR,"Error: Unexpected handle_get with fd=%d for request %s", fd, request); } return; // end of handle_action1() if we were with a pseudo object } else *pRequestPseudoFolder1 = '/'; // We temporarily had a '\0' here } // Normal (real file) sending follows *pRequestOffset = '\0'; // correctly finish filename string //loglevel(LOGDEBUG,"Debug: handle get: offset '%s' filename '%s'",pRequestOffset+1,pRequestFile); filename = pRequestFile; // Parse request to get 2 numbers: long long decimal in 'offsetRead' and 'c' (offsetBegin offsetEnd) if (sscanf(pRequestOffset + 1, "%lld %ld", &requestOffset, &requestChunk) != 2) { loglevel(LOGERROR,"Error: handle_get Invalid GET parameters '%s'", pRequestOffset + 1); return; }; // Automatic type conversion 'long long => off_t' and 'long => size_t' offset = requestOffset; chunk = requestChunk; requested_offset = offset; // Optimisation: Does the detection flag 'Filename is candidate to resize' should be done for each offset ? int willResizeJPEGFile = will_resize_jpeg_file(filename); // big step are .... if (fd == -1) { int isSubtitleFile = list_contains_extension(subtitle_file_extensions, filename); if (isSubtitleFile) { transform_subtitle_filename(filename); } int isActionPseudoFile = create_commandpseudofile_if_needed(filename); if (isActionPseudoFile && commandpseudofile_data) { sent = send_bytes_to_player((char *)commandpseudofile_data + offset, chunk); return; } if (verify_path(filename) < 0) { loglevel(LOGERROR,"Error: handle_get access denied: '%s'", filename); return; } if (run_trigger(config.pretrigger, filename, reqsize - (pRequestFile - request), tfilename, sizeof(tfilename)) < 0) { loglevel(LOGERROR,"Error: handle_get pretrigger failed '%s'", filename); return; } filename = tfilename; if (!willResizeJPEGFile) { if ((lfd = open(filename, O_RDONLY)) < 0) { if (!isSubtitleFile) loglevel(LOGERROR,"Error: handle_get open '%s': %s", filename, strerror(errno)); return; } } fd = lfd; // Optimisation: KiSS player request a picture with X request GET of 4096 bytes // We only need trigger, resize and create_recent once per picture (first request has requestOffset==0) if (config.max_recent_files && !isSubtitleFile && !isActionPseudoFile && !requestOffset) store_recent(filename); } // end if (fd== -1) if (willResizeJPEGFile) { if (offset == 0 && config.picturecachesize && config.picturecachesize != 1) piccache_wait_if_precaching_this(filename); if ((sent = CachedSendResizedJPEG(player_socket, filename, offset, chunk)) < 0) { loglevel(LOGERROR,"Error: CachedSendResizedJPEG: %s", strerror(errno)); run_trigger(config.posttrigger, filename, reqsize - (pRequestFile - request), NULL, 0); return; } } else { if (chunk == 0) { struct stat st; if (stat(filename, &st) < 0) { loglevel(LOGERROR,"Error: handle_get stat '%s': %s", filename, strerror(errno)); if (lfd != -1) { run_trigger(config.posttrigger, filename, reqsize - (pRequestFile - request), NULL, 0); close(lfd); } return; } chunk = st.st_size; } #ifdef USE_INTERNAL_SENDFILE if ((sent = my_internal_sendfile(player_socket, fd, &offset, chunk)) < 0) { #else #ifdef Linux if ((sent = sendfile(player_socket, fd, &offset, chunk)) < 0) { #else /* FreeBSD */ if (sendfile(fd, player_socket, offset, chunk, NULL, &sent, 0) < 0) { #endif #endif loglevel(LOGERROR,"Error: handle_get sendfile: '%s'", strerror(errno)); if (lfd != -1) { run_trigger(config.posttrigger, filename, reqsize - (pRequestFile - request), NULL, 0); close(lfd); } return; } } loglevel(LOGINFO,"--> [%d bytes at offset %lld]", sent, requested_offset); /* The client asked for more data than we have, send padding bytes */ if (sent < chunk) logv("--> [%d padding null bytes]", chunk - sent); while (sent++ < chunk) write(player_socket, "\0", 1); if (lfd != -1 || willResizeJPEGFile) { run_trigger(config.posttrigger, filename, reqsize - (pRequestFile - request), NULL, 0); if (lfd != -1) close(lfd); } } // ***** ACTION 1 and ACTION 2 management ********** static void handle_action1(char *request, size_t reqsize) { // request_size is received with 512 // ex:(DP558) ACTION 2 /home/olivier/media/video/{kissdx-DIR}HommeOrchestre/Title no. 01 of 01.mpg| // ex:(DP1500) ACTION 1 |abcdefghjikl| /media/video/JamesBond007.avi| // ex:(DP1500) ACTION 2 /media/Music/24_Various Artists_Top/101 - Track 1.mp3| // ex:(DP1500) ACTION 1 |abcdefghjikl| /media/Music/24_Various Artists_Top/101 - Track 1.mp3| loglevel(LOGINFO,"Info: Fct Handle_Action1 '%s'",request); char *pRequestFile, *pRequestOffset, *pRequestPseudoFolder1, *pRequestPseudoFolder2 ; int fd = -1; int len; char tfilename[PATH_MAX]; int willResizeJPEGFile = 0; isStoredInHistory = 0; // We store the filename only once in history folder (.recent) pRequestFile = strchr(request, '/'); // filename part of request pRequestOffset = strrchr(request, '|'); // offset part of request if (pRequestFile == NULL || pRequestOffset == NULL ) { loglevel(LOGERROR,"Error: Invalid ACTION command \"%s\"", request); return; } // Special handling of pseudo files for playing ISO images and VIDEO_TS folders, or back-to-back playback /* ISO or B2B has the identifier of pseudo "kissdx-" at different level : * ISO: 'ACTION 2 /home/olivier/media/video/{kissdx-DIR}HommeOrchestre/Title no. 01 of 01.mpg|' * pRequestPseudoFolder1 'Title no. 01 of 01.mpg|' * pRequestPseudoFolder2 '/{kissdx-DIR}HommeOrchestre' * B2B: 'ACTION 2 /home/olivier/media/musique/Idir/{kissdx-B2B}Idir.mp3|' * pRequestPseudoFolder1 '{kissdx-B2B}Idir.mp3|' * pRequestPseudoFolder2 '/Idir' * */ pRequestPseudoFolder1 = strrchr(request, '/'); // Search end of pseudo directory name requested if (pRequestPseudoFolder1) { *pRequestPseudoFolder1 = '\0'; pRequestPseudoFolder2 = strrchr(request, '/'); // Check starting string "{kissdx-" in pRequestPseudoFolderBegin and replace corresponding folder if ( (pRequestPseudoFolder2 && strncmp(pRequestPseudoFolder2 +1, C_ANY_PSEUDOFOLDER_PREFIX, strlen(C_ANY_PSEUDOFOLDER_PREFIX)) == 0) || ( strncmp(pRequestPseudoFolder1 +1, C_ANY_PSEUDOFOLDER_PREFIX, strlen(C_ANY_PSEUDOFOLDER_PREFIX)) == 0)) { //Either filename or last directory name is a "pseudo" information *pRequestPseudoFolder1 = '/'; // We temporarily had a '\0' here if (fd == -1) { // If nothing to read on the TCP stream (via fd) handle_action1_pseudofile(request, reqsize); } else { loglevel(LOGERROR,"Error: Unexpected handle_action1 with fd=%d for request %s", fd, request); } return; // end of handle_action1() if we were with a pseudo object } else *pRequestPseudoFolder1 = '/'; // We temporarily had a '\0' here } // Normal (real file) ACTION handling follows *pRequestOffset = '\0'; // remove the trailing '|' of ACTION2 request char *path = pRequestFile; int isSubtitleFile = list_contains_extension(subtitle_file_extensions, path); if (isSubtitleFile) transform_subtitle_filename(path); int isActionPseudoFile = create_commandpseudofile_if_needed(path); if (!isActionPseudoFile) { if (verify_path(path) < 0) { loglevel(LOGERROR,"Error: handle_action1: access denied: %s", path); send_bytes_to_player("404", 3); return; } if (run_trigger(config.pretrigger, path, reqsize - (pRequestFile - request), tfilename, sizeof(tfilename)) < 0) { loglevel(LOGERROR,"Error: handle_action1: pretrigger %s failed", path); send_bytes_to_player("404", 3); return; } path = tfilename; willResizeJPEGFile = will_resize_jpeg_file(path); if (!willResizeJPEGFile) { if ((fd = open(path, O_RDONLY)) < 0) { if (!isSubtitleFile) loglevel(LOGERROR,"Error: handle_action1: open %s: %s", path, strerror(errno)); send_bytes_to_player("404", 3); run_trigger(config.posttrigger, path, reqsize - (pRequestFile - request), NULL, 0); return; } } } // End of ACTION2 request: Send back '200' if (send_bytes_to_player("200", 3) < 0) { if (fd != -1) close(fd); if (!isActionPseudoFile) run_trigger(config.posttrigger, path, reqsize - (pRequestFile - request), NULL, 0); return; } // Store in history folder (.recent) if (config.max_recent_files && !isSubtitleFile && !isActionPseudoFile) { store_recent(path); isStoredInHistory = 1; } // Once ACTION2 received, the next request should be SIZE and a set of GET, until player close connection while (1) { static char newreq[512]; char prevcmd = *newreq; len = receive_line_from_player(newreq, sizeof(newreq), config.networktimeoutinterval); if (len < 0) { // Connection just be closed by player (full GET is finished) loglevel(LOGINFO,"Info handle_action1 request (%s) in child.id(%d)",newreq,getpid()); if (fd != -1) close(fd); if (!isActionPseudoFile) run_trigger(config.posttrigger, path, reqsize - (pRequestFile - request), NULL, 0); // After last GET of current picture, try caching next one if (prevcmd == 'G' && config.picturecachesize && config.picturecachesize != 1 && willResizeJPEGFile) precache_next_picture(path); // After last GET of Command Pseudofile, perform the command and release confirmation jpeg memory if (prevcmd == 'G' && isActionPseudoFile) { if (pseudofile_command != pfc_none) perform_pseudofile_command(); if (commandpseudofile_data) { free_gd_mem(commandpseudofile_data); commandpseudofile_data = NULL; commandpseudofile_size = 0; } } return; } // end if connection just closed transform_requested_filename(newreq); // Handle a SIZE command following an ACTION2 on the same TCP connection if (!strncmp(newreq, "SIZE", 4)) { handle_size(newreq, 512); } // Handle a GET command following an ACTION2+SIZE on the same TCP connection else if (!strncmp(newreq, "GET", 3)) { handle_get(newreq, 512, fd); } else loglevel(LOGERROR,"Error: unknown ACTION command \"%s\"", newreq); } } // ********* Treat any new request on PC-LINK protocol *********** void handle_pclink_request(int sock) { int timeout = 0; // We know we have data waiting when called player_socket = sock; // Store in common variable to be used by all send/receive functions while (1) { static char request[512]; int len; if ((len = receive_line_from_player(request, sizeof(request), timeout)) < 0) { CachedDvdClose(); CachedB2bClose(); return; } // // ############# DEBUGGING - REMOVE BELOW!!!!! ########### 2006-10-22/VT // static int isFirstRequest = 1; // if (isFirstRequest) { // char userinputbuf[255]; // fputs("New process started. Press Enter to continue (after attaching debugger).", stderr); // fgets(userinputbuf, sizeof userinputbuf, stdin); // isFirstRequest = 0; // } // // ############# DEBUGGING - REMOVE ABOVE!!!!! ########### 2006-10-22/VT loglevel(LOGPROTO,"Proto:PcLink request:[%s]",request); transform_requested_filename(request); //replace .recent and pseudofile loglevel(LOGDEBUG,"Debug:PcLink post transform request:[%s]",request); if (!strncmp(request, "LIST", 4)) handle_list(request, 512); else if (!strncmp(request, "ACTION 1", 8)) handle_action1(request, 512); else if (!strncmp(request, "ACTION 2", 8)) handle_action1(request, 512); else if (!strncmp(request, "SIZE", 4)) handle_size(request, 512); else if (!strncmp(request, "GET", 3)) handle_get(request, 512, -1); else log("unknown KiSS command \"%s\"", request); timeout = config.networktimeoutinterval; } } void handle_kml_request(int sock) { char request[512]; int len; char *ok1 = "\n\n\n\n"; char *error = "\n501 Method Not Implemented\n\n

Method Not Implemented

\n\n"; player_socket = sock; // Store in common variable to be used by all send/receive functions if ((len = receive_line_from_player(request, sizeof(request), 0)) < 0) return; if (!strncmp(request, "GET /index.kml", 14)) { send_bytes_to_player(ok1, strlen(ok1)); send_bytes_to_player(config.kmlfwdurl, strlen(config.kmlfwdurl)); send_bytes_to_player(ok2, strlen(ok2)); } else { send_bytes_to_player(error, strlen(error)); log("unknown KiSS kml command \"%s\"", request); } } /* * Name: store_recent * * Description: Writes currently requested file to recently used * direcory list. */ static int store_recent(char *filename) { // Store the filename symblink to history folder // Storage occurs following a PcLink 'ACTION2' (not a LIST) // . // PSEUDOFILE Sample :DIR or ISO // Viewed following a LIST // [Title no. 01 of 01.mpg|/home/olivier/media/video/{kissdx-DIR}HommeOrchestre/Title no. 01 of 01.mpg|0|] // ACTION2 on filename = '/home/olivier/media/video/{kissdx-DIR}HommeOrchestre/Title no. 01 of 01.mpg // will create a text file containing reference, not a symlink // (A new file in .recent) = {kissdx-DIR}HommeOrchestre.mpg // (containing :) // /home/olivier/media/video/{kissdx-DIR}HommeOrchestre/Title no. 01 of 01.mpg // loglevel(LOGINFO,"Info: store_recent: Trying to store in history folder '%s'", filename); char recent[PATH_MAX]; // fullpath of history folder (aka recently used folder) char target[PATH_MAX]; // Target of the symlink to be created char fpath[PATH_MAX]; // Name of the symlink to be create char *thisname; int n, num_files, len, file_no; struct dirent **namelist; int isPseudoFile = strstr(filename, "/"C_DIR_PSEUDOFOLDER_PREFIX) || strstr(filename, "/"C_ISO_PSEUDOFOLDER_PREFIX); struct stat statbuf; int stat_rc; // stat return code // As hidden files picture are precached to enable performance // we may store the symblink as hidden one (starting with '.') if (strstr(filename, "/.")) { loglevel(LOGDEBUG,"Debug: store_recent: Hidden file is not store in history folder '%s'", filename); return 0; // We don't store hidden files } if (get_recent_path(recent, filename)) { loglevel(LOGERROR,"Error: store_recent: Could not produce recently used directory for filename '%s'", filename); return -1; } if (mkdir(recent, standard_dir_filemode) < 0) { // User.RWX + Grp.RX + Other.RX if (errno != EEXIST) { // All other case than EEXIST= A file named filename already exists. loglevel(LOGERROR,"Error: store_recent: Couldn't create directory '%s': %s", recent, strerror(errno)); return -1; } } else if (chmod(recent, standard_dir_filemode) < 0) loglevel(LOGERROR,"Error: store_recent: Cannot chmod directory '%s': %s", recent, strerror(errno)); chdir(recent); /* scandir does not provide path info to sort */ num_files = scandir(recent, &namelist, (void*)NULL, (void*)timesort); if (num_files < 0) loglevel(LOGERROR,"Error: store_recent: scandir failed '%s': %s", recent, strerror(errno)); else { // Scan all existing file in history (.recent) folder for (file_no = 1, n = 0; n < num_files; n++) { thisname = namelist[n]->d_name; /* skip parent and current dir */ if (!strcmp (thisname, ".") || !strcmp (thisname, "..")) { free(namelist[n]); continue; } loglevel(LOGDEBUG,"Debug: trim history max(%d) index(%d) file:'%s'",config.max_recent_files,file_no,thisname); /* Based on MaxRecentFile option, purge oldest files */ if (file_no > config.max_recent_files) { snprintf(fpath, PATH_MAX, "%s/%s", recent, thisname); loglevel(LOGINFO,"Info: MaxRecent(%d) Delete oldest recent entry '%s'",config.max_recent_files, fpath); // The unlink function deletes the file named filename. // If this is a file's sole name, the file itself is also deleted. // Deletion of media is not acceptable, then secure the action with stat() S_ISLNK if (lstat(fpath, &statbuf) < 0) { loglevel(LOGERROR,"Error: store_recent purge oldest symblink: Couldn't stat '%s'", fpath); return -1; } if (S_ISLNK(statbuf.st_mode)) { // This macro returns non-zero if the file is a symbolic link if (unlink(fpath) < 0) { loglevel(LOGERROR,"Error: store_recent purge oldest symblink: Couldn't unlink '%s'", fpath); } else { loglevel(LOGDEBUG,"Debug: store_recent purge oldest symblink: Unlinked '%s'", fpath); } } // endif S_ISLNK } // endif purge oldest recent entries free(namelist[n]); file_no++; } free(namelist); } if (!strncmp(filename, recent, strlen(recent))) // the current filename is a symbolic link from history folder (.recent path) // Two possibilities : Standardfile_Symlink or PseudoFile_Symlink // (.recent content) MyAlbumAAAA.mp3 -> /home/olivier/media/musique/Idir/MyAlbumAAAA.mp3 // (.recent content) {kissdx-DIR}HommeOrchestre.mpg (txt file) containing one line with full real pathname // Goal: Populate 'target' with the physical file to stream if (isPseudoFile) { // Example --> [ {kissdx-DIR}HommeOrchestre.mpg (txt file) - aka 'link file' int fd; // file descriptor int charsread; if ((fd = open(filename, O_RDONLY)) < 0) { // check readness of filename to store in .recent loglevel(LOGERROR,"Error store_recent: Cannot open link file '%s': %s", filename, strerror(errno)); return -1; } if ((charsread = read(fd, target, sizeof target)) < 0) // search refered file in the symblink loglevel(LOGERROR,"Error store_recent: Cannot read from link file %s: %s", filename, strerror(errno)); else *(target + charsread) = '\0'; // finish the charset to be compliant to a C string close(fd); } else { // classical filename if ((n = readlink(filename, target, PATH_MAX)) < 0) { loglevel(LOGERROR,"Error store_recent: Cannot read from link file '%s': %s", filename, strerror(errno)); return -1; } else *(target + n) = '\0'; } else // Current filename is a standard filename in mediastore directory (not a pseudofile, nor one from .recent dir strncpy(target, filename, PATH_MAX); if (isPseudoFile) { //example: {kissdx-DIR}HommeOrchestre.mpg that refer to directory /HommeOrchestre/ with VIDEO_TS ... // filename = /home/olivier/media/video/{kissdx-DIR}HommeOrchestre/Title no. 01 of 01.mpg // target will be = /home/olivier/media/video/{kissdx-DIR}HommeOrchestre.mpg *(strrchr(target, '/')) = '\0'; // Strip the title from a DVD entry (close string on last right /) strcat(target, strrchr(filename, '.')); // And add the ".mpg" extension instead } // Generate symlink name : (dir of history (.recent))+ basename(target) if ((len = snprintf(fpath, sizeof fpath, "%s/%s", recent, basename(target))) >= sizeof fpath) { loglevel(LOGERROR,"Error store_recent: Buffer too small for new recent filename '%s'", basename(target)); return -1; } // Make sure the recent file link is created as a new one (we delete the old one now) stat_rc=lstat(fpath, &statbuf); if ( stat_rc < 0 && errno != ENOENT) { // ENOENT:The file named doesn't exist. (first time try to create symlink) loglevel(LOGERROR,"Error store_recent refresh symblink: Couldn't stat '%s': %s", fpath, strerror(errno)); return -1; } else if (errno != ENOENT && S_ISLNK(statbuf.st_mode) ) { // This macro returns non-zero if the file is a symbolic link if (unlink(fpath) < 0) { loglevel(LOGERROR,"Error store_recent refresh symblink: Couldn't unlink '%s': %s", fpath, strerror(errno)); return -1; } else { loglevel(LOGDEBUG,"Debug store_recent refresh symblink: Unlinked '%s'", fpath); } } // endif S_ISLNK if (isPseudoFile) { // Generate a link file (txt file), since a symlink cannot link to a pseudo-file int fd, written; if ((fd = open(fpath, O_CREAT | O_WRONLY | O_TRUNC, standard_filemode)) < 0) { loglevel(LOGERROR,"Error store_recent: cannot create link file '%s': %s", fpath, strerror(errno)); return -1; } if ((written = write(fd, filename, strlen(filename) + 1)) <= strlen(filename)) { loglevel(LOGERROR,"Error store_recent: cannot write to link file '%s': %s", fpath, strerror(errno)); close(fd); // close file before quiting in error return -1; } close(fd); // correctly created, we close the symlink fullfilled } else // Standard file could have a classical symlink if (symlink(target, fpath) < 0) { if (errno != EEXIST) { // EEXIST : the symlink already exist loglevel(LOGERROR,"Error store_recent: cannot create symlink '%s' to '%s': %s", fpath, target, strerror(errno)); return -1; } } else loglevel(LOGINFO,"Info: store_recent: Created symlink '%s' refering --> '%s'", fpath, target); return 0; } /* Transform the filename in the received request before processing the request. * * We process ACTION, SIZE and GET requests, but not LIST requests. * * In all requests: * * - Remove the sequence number that is added in all the outgoing filenames when * config setting displaysequencenumbers = yes. * * In requests for DVD pseudo-files in the .recent directory: * * - Simulate symlink behaviour by replacing the filename in a request for a link * file from the .recent directory with the filename that the link file links to. * * In requests for all other files in the .recent directory: * * - Replace the filename in a request for a link file from the .recent directory with * the actual symlink target, so that * - a scaled picture file will be served from the cache if it has been cached * - a back-to-back combined file will have the correct path to find its media files * - a renamed file will have the correct path to find the real media file * * In requests for all non-pseudo files regardless of directory: * * - Detect a request for a renamed filetype and rename the filetype in the request * back to the filetype of the real file on disk. * * As a result of this, we never actually exercise the symlinks in .recent to stream from * the media file. Instead we get the real filename from the symlink and use that filename. * * OPTIMISATION OF REPEATED PROCESSING: * * We store in previous_requested_filename the original filename from the previous * request that was transformed. * We store in previous_transformed_filename the transformed filename from same. * We detect when this request and the previous request are for the same filename, * and just replace previous_requested_filename with previous_transformed_filename * in the request if that is the case (which it will be most of the time). * * Implementation notes: * * - The incoming request is at maximum 512 characters in size, i.e. max 511 chars long. * * */ static void transform_requested_filename(char *request) { static char previous_requested_filename[512] = ""; static char previous_transformed_filename[PATH_MAX] = ""; char *p1, *p2, *p3; char rest[512]; static const char *recent_pathpart = "/.recent/"; static int recent_pathpart_len = 0; if (recent_pathpart_len == 0) recent_pathpart_len = strlen(recent_pathpart); if (strncmp(request, "LIST ", 5)) { p1 = strchr(request, '/'); p2 = strchr(request, '|'); if (p1 && p2 && p2 > p1) { // This looks like a valid request strcpy(rest, p2); // Temporarily store tail part of request (part after filename) *p2 = '\0'; // Temporarily replace '|' with '\0' for convenient handling of filename in p1 // OPTIMISATION OF REPEATED PROCESSING if (!strcmp(p1, previous_requested_filename)) { loglevel(LOGDEBUG,"Debug: transform_requested_filename Optim TF File %s",previous_requested_filename); strcpy(p1, previous_transformed_filename); strcat(p1, rest); return; // We are done: No need to do logic below because we did it last time } // NORMAL PROCESSING (THIS IS NOT A REPEATED REQUEST FOR THE SAME FILE) // We must remove the optionally added sequence number from all incoming filenames remove_seqno_from_incoming_filename(p1); strcpy(previous_requested_filename, p1); if ((p3 = strstr(p1, recent_pathpart)) && p3 < p2 - recent_pathpart_len) { if (!strncmp(p3 + recent_pathpart_len, C_DIR_PSEUDOFOLDER_PREFIX, strlen(C_DIR_PSEUDOFOLDER_PREFIX)) || !strncmp(p3 + recent_pathpart_len, C_ISO_PSEUDOFOLDER_PREFIX, strlen(C_ISO_PSEUDOFOLDER_PREFIX))) { // This is a request for a DVD pseudo-file in the .recent directory int fd, charsread; if ((fd = open(p1, O_RDONLY)) >= 0) { if ((charsread = read(fd, p1, 500 - (p1 - request))) >= 0) { *(p1 + charsread) = '\0'; } else { log("transform_requested_filename: cannot read from link file: %s", strerror(errno)); } close(fd); } else { log("transform_requested_filename: cannot open link file %s: %s", p1, strerror(errno)); } } else { // This is a request for another type of file in the .recent directory char target[PATH_MAX]; int n; if ((n = readlink(p1, target, PATH_MAX)) > 0) { *(target + n) = '\0'; strcpy(p1, target); } } p2 = p1 + strlen(p1); // Adjust p2 in case a transformation was done } if (!strstr(p1, "/"C_DIR_PSEUDOFOLDER_PREFIX) && !strstr(p1, "/"C_ISO_PSEUDOFOLDER_PREFIX) && !strstr(p1, "/"C_B2B_PSEUDOFOLDER_PREFIX)) { // This is a request for a non-pseudo file // (but it may be a file inside a playlist or inside an "extra folder") // Change the filename extension if it has an extension that is to be renamed back // and there is no real file with the exact filename given in the request. rename_filetype_incoming(p1); } strcpy(previous_transformed_filename, p1); // Save transformed filename for use in optimization strcat(p1, rest); // Recompose request by restoring the tail part } } }