diff --git a/API.md b/API.md index ecdc127b..427dbc6b 100644 --- a/API.md +++ b/API.md @@ -1,10 +1,11 @@ # API Reference + The API is still pretty new and needs some serious cleaning up on the backend but should be reasonably functional. There are no error codes yet. ## General structure -The API endpoint is `http://ip:port + HTTP_ROOT + /api?apikey=$apikey&cmd=$command` +The API endpoint is `http://ip:port + HTTP_ROOT + /api/v2?apikey=$apikey&cmd=$command` -Response example +Response example (default `json`) ``` { "response": { @@ -21,60 +22,1620 @@ Response example } } ``` +``` +General optional parameters: -General parameters: - out_type: 'xml', - callback: 'pong', - 'debug': 1 - + out_type: "json" or "xml" + callback: "pong" + debug: 1 +``` ## API methods -### getLogs -Possible params: sort='', search='', order='desc', regex='', start=0, end=0 -Returns the plexpy log +### arnold +Get to the chopper! -### getApikey -Possible params: username='', password='' (required if auth is enabled) -Returns the apikey -### getSettings -No params -Returns the config file +### backup_config +Create a manual backup of the `config.ini` file. -### getVersion -No params -Returns some version information: git_path, install_type, current_version, installed_version, commits_behind -### getHistory -possible params: user=None, user_id=None, ,rating_key='', parent_rating_key='', grandparent_rating_key='', start_date='' -Returns +### backup_db +Create a manual backup of the `plexpy.db` file. -### getMetadata -Required params: rating_key -Returns metadata about a file -### getSync -Possible params: machine_id=None, user_id=None, -Returns +### delete_all_library_history +Delete all PlexPy history for a specific library. -### getUserips -Possible params: user_id=None, user=None +``` +Required parameters: + section_id (str): The id of the Plex library section -### getPlayby -Possible params: time_range=30, y_axis='plays', playtype='total_plays_per_month' +Optional parameters: + None -### checkGithub -Updates the version information above and returns getVersion data +Returns: + None +``` + + +### delete_all_user_history +Delete all PlexPy history for a specific user. + +``` +Required parameters: + user_id (str): The id of the Plex user + +Optional parameters: + None + +Returns: + None +``` + + +### delete_cache +Delete and recreate the cache directory. + + +### delete_datatable_media_info_cache +Delete the media info table cache for a specific library. + +``` +Required parameters: + section_id (str): The id of the Plex library section + +Optional parameters: + None + +Returns: + None +``` + + +### delete_image_cache +Delete and recreate the image cache directory. + + +### delete_library +Delete a library section from PlexPy. Also erases all history for the library. + +``` +Required parameters: + section_id (str): The id of the Plex library section + +Optional parameters: + None + +Returns: + None +``` + + +### delete_login_log +Delete the PlexPy login logs. + +``` +Required paramters: + None + +Optional parameters: + None + +Returns: + None +``` + + +### delete_notification_log +Delete the PlexPy notification logs. + +``` +Required paramters: + None + +Optional parameters: + None + +Returns: + None +``` + + +### delete_user +Delete a user from PlexPy. Also erases all history for the user. + +``` +Required parameters: + user_id (str): The id of the Plex user + +Optional parameters: + None + +Returns: + None +``` + + +### docs +Return the api docs as a dict where commands are keys, docstring are value. + + +### docs_md +Return the api docs formatted with markdown. + + +### download_log +Download the PlexPy log file. + + +### edit_library +Update a library section on PlexPy. + +``` +Required parameters: + section_id (str): The id of the Plex library section + +Optional parameters: + custom_thumb (str): The URL for the custom library thumbnail + do_notify (int): 0 or 1 + do_notify_created (int): 0 or 1 + keep_history (int): 0 or 1 + +Returns: + None +``` + + +### edit_user +Update a user on PlexPy. + +``` +Required parameters: + user_id (str): The id of the Plex user + +Optional paramters: + friendly_name(str): The friendly name of the user + custom_thumb (str): The URL for the custom user thumbnail + do_notify (int): 0 or 1 + do_notify_created (int): 0 or 1 + keep_history (int): 0 or 1 + +Returns: + None +``` + + +### get_activity +Get the current activity on the PMS. + +``` +Required parameters: + None + +Optional parameters: + None + +Returns: + json: + {"stream_count": 3, + "session": + [{"art": "/library/metadata/1219/art/1462175063", + "aspect_ratio": "1.78", + "audio_channels": "6", + "audio_codec": "ac3", + "audio_decision": "transcode", + "bif_thumb": "/library/parts/274169/indexes/sd/", + "bitrate": "10617", + "container": "mkv", + "content_rating": "TV-MA", + "duration": "2998290", + "friendly_name": "Mother of Dragons", + "grandparent_rating_key": "1219", + "grandparent_thumb": "/library/metadata/1219/thumb/1462175063", + "grandparent_title": "Game of Thrones", + "height": "1078", + "indexes": 1, + "ip_address": "xxx.xxx.xxx.xxx", + "labels": [], + "machine_id": "83f189w617623ccs6a1lqpby", + "media_index": "1", + "media_type": "episode", + "parent_media_index": "6", + "parent_rating_key": "153036", + "parent_thumb": "/library/metadata/153036/thumb/1462175062", + "parent_title": "", + "platform": "Chrome", + "player": "Plex Web (Chrome)", + "progress_percent": "0", + "rating_key": "153037", + "section_id": "2", + "session_key": "291", + "state": "playing", + "throttled": "1", + "thumb": "/library/metadata/153037/thumb/1462175060", + "title": "The Red Woman", + "transcode_audio_channels": "2", + "transcode_audio_codec": "aac", + "transcode_container": "mkv", + "transcode_height": "1078", + "transcode_key": "tiv5p524wcupe8nxegc26s9k9", + "transcode_progress": 2, + "transcode_protocol": "http", + "transcode_speed": "0.0", + "transcode_video_codec": "h264", + "transcode_width": "1920", + "user": "DanyKhaleesi69", + "user_id": 8008135, + "user_thumb": "https://plex.tv/users/568gwwoib5t98a3a/avatar", + "video_codec": "h264", + "video_decision": "copy", + "video_framerate": "24p", + "video_resolution": "1080", + "view_offset": "", + "width": "1920", + "year": "2016" + }, + {...}, + {...} + ] + } +``` + + +### get_apikey +Get the apikey. Username and password are required +if auth is enabled. Makes and saves the apikey if it does not exist. + +``` +Required parameters: + None + +Optional parameters: + username (str): Your PlexPy username + password (str): Your PlexPy password + +Returns: + string: "apikey" +``` + + +### get_date_formats +Get the date and time formats used by PlexPy. + + ``` +Required parameters: + None + +Optional parameters: + None + +Returns: + json: + {"date_format": "YYYY-MM-DD", + "time_format": "HH:mm", + } +``` + + +### get_history +Get the PlexPy history. + +``` +Required parameters: + None + +Optional parameters: + grouping (int): 0 or 1 + user (str): "Jon Snow" + user_id (int): 133788 + rating_key (int): 4348 + parent_rating_key (int): 544 + grandparent_rating_key (int): 351 + start_date (str): "YYYY-MM-DD" + section_id (int): 2 + media_type (str): "movie", "episode", "track" + transcode_decision (str): "direct play", "copy", "transcode", + order_column (str): "date", "friendly_name", "ip_address", "platform", "player", + "full_title", "started", "paused_counter", "stopped", "duration" + order_dir (str): "desc" or "asc" + start (int): Row to start from, 0 + length (int): Number of items to return, 25 + search (str): A string to search for, "Thrones" + +Returns: + json: + {"draw": 1, + "recordsTotal": 1000, + "recordsFiltered": 250, + "total_duration": "42 days 5 hrs 18 mins", + "filter_duration": "10 hrs 12 mins", + "data": + [{"year": 2016, + "paused_counter": 0, + "player": "Plex Web (Chrome)", + "parent_rating_key": 544, + "parent_title": "", + "duration": 263, + "transcode_decision": "transcode", + "rating_key": 4348, + "user_id": 8008135, + "thumb": "/library/metadata/4348/thumb/1462414561", + "id": 1124, + "platform": "Chrome", + "media_type": "episode", + "grandparent_rating_key": 351, + "started": 1462688107, + "full_title": "Game of Thrones - The Red Woman", + "reference_id": 1123, + "date": 1462687607, + "percent_complete": 84, + "ip_address": "xxx.xxx.xxx.xxx", + "group_ids": "1124", + "media_index": 17, + "friendly_name": "Mother of Dragons", + "watched_status": 0, + "group_count": 1, + "stopped": 1462688370, + "parent_media_index": 7, + "user": "DanyKhaleesi69" + }, + {...}, + {...} + ] + } +``` + + +### get_home_stats +Get the homepage watch statistics. + +``` +Required parameters: + None + +Optional parameters: + grouping (int): 0 or 1 + time_range (str): The time range to calculate statistics, '30' + stats_type (int): 0 for plays, 1 for duration + stats_count (str): The number of top items to list, '5' + +Returns: + json: + [{"stat_id": "top_movies", + "stat_type": "total_plays", + "rows": [{...}] + }, + {"stat_id": "popular_movies", + "rows": [{...}] + }, + {"stat_id": "top_tv", + "stat_type": "total_plays", + "rows": + [{"content_rating": "TV-MA", + "friendly_name": "", + "grandparent_thumb": "/library/metadata/1219/thumb/1462175063", + "labels": [], + "last_play": 1462380698, + "media_type": "episode", + "platform": "", + "platform_type": "", + "rating_key": 1219, + "row_id": 1116, + "section_id": 2, + "thumb": "", + "title": "Game of Thrones", + "total_duration": 213302, + "total_plays": 69, + "user": "", + "users_watched": "" + }, + {...}, + {...} + ] + }, + {"stat_id": "popular_tv", + "rows": [{...}] + }, + {"stat_id": "top_music", + "stat_type": "total_plays", + "rows": [{...}] + }, + {"stat_id": "popular_music", + "rows": [{...}] + }, + {"stat_id": "last_watched", + "rows": [{...}] + }, + {"stat_id": "top_users", + "stat_type": "total_plays", + "rows": [{...}] + }, + {"stat_id": "top_platforms", + "stat_type": "total_plays", + "rows": [{...}] + }, + {"stat_id": "most_concurrent", + "rows": [{...}] + } + ] +``` + + +### get_libraries +Get a list of all libraries on your server. + +``` +Required parameters: + None + +Optional parameters: + None + +Returns: + json: + [{"art": "/:/resources/show-fanart.jpg", + "child_count": "3745", + "count": "62", + "parent_count": "240", + "section_id": "2", + "section_name": "TV Shows", + "section_type": "show", + "thumb": "/:/resources/show.png" + }, + {...}, + {...} + ] +``` + + +### get_libraries_table +Get the data on the PlexPy libraries table. + +``` +Required parameters: + None + +Optional parameters: + order_column (str): "library_thumb", "section_name", "section_type", "count", "parent_count", + "child_count", "last_accessed", "last_played", "plays", "duration" + order_dir (str): "desc" or "asc" + start (int): Row to start from, 0 + length (int): Number of items to return, 25 + search (str): A string to search for, "Movies" + +Returns: + json: + {"draw": 1, + "recordsTotal": 10, + "recordsFiltered": 10, + "data": + [{"child_count": 3745, + "content_rating": "TV-MA", + "count": 62, + "do_notify": "Checked", + "do_notify_created": "Checked", + "duration": 1578037, + "id": 1128, + "keep_history": "Checked", + "labels": [], + "last_accessed": 1462693216, + "last_played": "Game of Thrones - The Red Woman", + "library_art": "/:/resources/show-fanart.jpg", + "library_thumb": "", + "media_index": 1, + "media_type": "episode", + "parent_count": 240, + "parent_media_index": 6, + "parent_title": "", + "plays": 772, + "rating_key": 153037, + "section_id": 2, + "section_name": "TV Shows", + "section_type": "Show", + "thumb": "/library/metadata/153036/thumb/1462175062", + "year": 2016 + }, + {...}, + {...} + ] + } +``` + + +### get_library_media_info +Get the data on the PlexPy media info tables. + +``` +Required parameters: + section_id (str): The id of the Plex library section, OR + rating_key (str): The grandparent or parent rating key + +Optional parameters: + section_type (str): "movie", "show", "artist", "photo" + order_column (str): "added_at", "title", "container", "bitrate", "video_codec", + "video_resolution", "video_framerate", "audio_codec", "audio_channels", + "file_size", "last_played", "play_count" + order_dir (str): "desc" or "asc" + start (int): Row to start from, 0 + length (int): Number of items to return, 25 + search (str): A string to search for, "Thrones" + +Returns: + json: + {"draw": 1, + "recordsTotal": 82, + "recordsFiltered": 82, + "filtered_file_size": 2616760056742, + "total_file_size": 2616760056742, + "data": + [{"added_at": "1403553078", + "audio_channels": "", + "audio_codec": "", + "bitrate": "", + "container": "", + "file_size": 253660175293, + "grandparent_rating_key": "", + "last_played": 1462380698, + "media_index": "1", + "media_type": "show", + "parent_media_index": "", + "parent_rating_key": "", + "play_count": 15, + "rating_key": "1219", + "section_id": 2, + "section_type": "show", + "thumb": "/library/metadata/1219/thumb/1436265995", + "title": "Game of Thrones", + "video_codec": "", + "video_framerate": "", + "video_resolution": "", + "year": "2011" + }, + {...}, + {...} + ] + } +``` + + +### get_library_names +Get a list of library sections and ids on the PMS. + +``` +Required parameters: + None + +Optional parameters: + None + +Returns: + json: + [{"section_id": 1, "section_name": "Movies"}, + {"section_id": 7, "section_name": "Music"}, + {"section_id": 2, "section_name": "TV Shows"}, + {...} + ] +``` + + +### get_logs +Get the PlexPy logs. + +``` +Required parameters: + None + +Optional parameters: + sort (str): "time", "thread", "msg", "loglevel" + search (str): A string to search for + order (str): "desc" or "asc" + regex (str): A regex string to search for + start (int): Row number to start from + end (int): Row number to end at + +Returns: + json: + [{"loglevel": "DEBUG", + "msg": "Latest version is 2d10b0748c7fa2ee4cf59960c3d3fffc6aa9512b", + "thread": "MainThread", + "time": "2016-05-08 09:36:51 " + }, + {...}, + {...} + ] +``` + + +### get_metadata +Get the metadata for a media item. + +``` +Required parameters: + rating_key (str): Rating key of the item + media_info (bool): True or False wheter to get media info + +Optional parameters: + None + +Returns: + json: + {"metadata": + {"actors": [ + "Kit Harington", + "Emilia Clarke", + "Isaac Hempstead-Wright", + "Maisie Williams", + "Liam Cunningham", + ], + "added_at": "1461572396", + "art": "/library/metadata/1219/art/1462175063", + "content_rating": "TV-MA", + "directors": [ + "Jeremy Podeswa" + ], + "duration": "2998290", + "genres": [ + "Adventure", + "Drama", + "Fantasy" + ], + "grandparent_rating_key": "1219", + "grandparent_thumb": "/library/metadata/1219/thumb/1462175063", + "grandparent_title": "Game of Thrones", + "guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en", + "labels": [], + "last_viewed_at": "1462165717", + "library_name": "TV Shows", + "media_index": "1", + "media_type": "episode", + "originally_available_at": "2016-04-24", + "parent_media_index": "6", + "parent_rating_key": "153036", + "parent_thumb": "/library/metadata/153036/thumb/1462175062", + "parent_title": "", + "rating": "7.8", + "rating_key": "153037", + "section_id": "2", + "studio": "HBO", + "summary": "Jon Snow is dead. Daenerys meets a strong man. Cersei sees her daughter again.", + "tagline": "", + "thumb": "/library/metadata/153037/thumb/1462175060", + "title": "The Red Woman", + "updated_at": "1462175060", + "writers": [ + "David Benioff", + "D. B. Weiss" + ], + "year": "2016" + } + } +``` + + +### get_new_rating_keys +Get a list of new rating keys for the PMS of all of the item's parent/children. + +``` +Required parameters: + rating_key (str): '12345' + media_type (str): "movie", "show", "season", "episode", "artist", "album", "track" + +Optional parameters: + None + +Returns: + json: + {} +``` + + +### get_notification_log +Get the data on the PlexPy notification logs table. + +``` +Required parameters: + None + +Optional parameters: + order_column (str): "timestamp", "agent_name", "notify_action", + "subject_text", "body_text", "script_args" + order_dir (str): "desc" or "asc" + start (int): Row to start from, 0 + length (int): Number of items to return, 25 + search (str): A string to search for, "Telegram" + +Returns: + json: + {"draw": 1, + "recordsTotal": 1039, + "recordsFiltered": 163, + "data": + [{"agent_id": 13, + "agent_name": "Telegram", + "body_text": "Game of Thrones - S06E01 - The Red Woman [Transcode].", + "id": 1000, + "notify_action": "play", + "poster_url": "http://i.imgur.com/ZSqS8Ri.jpg", + "rating_key": 153037, + "script_args": "[]", + "session_key": 147, + "subject_text": "PlexPy (Winterfell-Server)", + "timestamp": 1462253821, + "user": "DanyKhaleesi69", + "user_id": 8008135 + }, + {...}, + {...} + ] + } +``` + + +### get_old_rating_keys +Get a list of old rating keys from the PlexPy database for all of the item's parent/children. + +``` +Required parameters: + rating_key (str): '12345' + media_type (str): "movie", "show", "season", "episode", "artist", "album", "track" + +Optional parameters: + None + +Returns: + json: + {} +``` + + +### get_plays_by_date +Get graph data by date. + +``` +Required parameters: + None + +Optional parameters: + time_range (str): The number of days of data to return + y_axis (str): "plays" or "duration" + user_id (str): The user id to filter the data + +Returns: + json: + {"categories": + ["YYYY-MM-DD", "YYYY-MM-DD", ...] + "series": + [{"name": "Movies", "data": [...]} + {"name": "TV", "data": [...]}, + {"name": "Music", "data": [...]} + ] + } +``` + + +### get_plays_by_dayofweek +Get graph data by day of the week. + +``` +Required parameters: + None + +Optional parameters: + time_range (str): The number of days of data to return + y_axis (str): "plays" or "duration" + user_id (str): The user id to filter the data + +Returns: + json: + {"categories": + ["Sunday", "Monday", "Tuesday", ..., "Saturday"] + "series": + [{"name": "Movies", "data": [...]} + {"name": "TV", "data": [...]}, + {"name": "Music", "data": [...]} + ] + } +``` + + +### get_plays_by_hourofday +Get graph data by hour of the day. + +``` +Required parameters: + None + +Optional parameters: + time_range (str): The number of days of data to return + y_axis (str): "plays" or "duration" + user_id (str): The user id to filter the data + +Returns: + json: + {"categories": + ["00", "01", "02", ..., "23"] + "series": + [{"name": "Movies", "data": [...]} + {"name": "TV", "data": [...]}, + {"name": "Music", "data": [...]} + ] + } +``` + + +### get_plays_by_source_resolution +Get graph data by source resolution. + +``` +Required parameters: + None + +Optional parameters: + time_range (str): The number of days of data to return + y_axis (str): "plays" or "duration" + user_id (str): The user id to filter the data + +Returns: + json: + {"categories": + ["720", "1080", "sd", ...] + "series": + [{"name": "Direct Play", "data": [...]} + {"name": "Direct Stream", "data": [...]}, + {"name": "Transcode", "data": [...]} + ] + } +``` + + +### get_plays_by_stream_resolution +Get graph data by stream resolution. + +``` +Required parameters: + None + +Optional parameters: + time_range (str): The number of days of data to return + y_axis (str): "plays" or "duration" + user_id (str): The user id to filter the data + +Returns: + json: + {"categories": + ["720", "1080", "sd", ...] + "series": + [{"name": "Direct Play", "data": [...]} + {"name": "Direct Stream", "data": [...]}, + {"name": "Transcode", "data": [...]} + ] + } +``` + + +### get_plays_by_stream_type +Get graph data by stream type by date. + +``` +Required parameters: + None + +Optional parameters: + time_range (str): The number of days of data to return + y_axis (str): "plays" or "duration" + user_id (str): The user id to filter the data + +Returns: + json: + {"categories": + ["YYYY-MM-DD", "YYYY-MM-DD", ...] + "series": + [{"name": "Direct Play", "data": [...]} + {"name": "Direct Stream", "data": [...]}, + {"name": "Transcode", "data": [...]} + ] + } +``` + + +### get_plays_by_top_10_platforms +Get graph data by top 10 platforms. + +``` +Required parameters: + None + +Optional parameters: + time_range (str): The number of days of data to return + y_axis (str): "plays" or "duration" + user_id (str): The user id to filter the data + +Returns: + json: + {"categories": + ["iOS", "Android", "Chrome", ...] + "series": + [{"name": "Movies", "data": [...]} + {"name": "TV", "data": [...]}, + {"name": "Music", "data": [...]} + ] + } +``` + + +### get_plays_by_top_10_users +Get graph data by top 10 users. + +``` +Required parameters: + None + +Optional parameters: + time_range (str): The number of days of data to return + y_axis (str): "plays" or "duration" + user_id (str): The user id to filter the data + +Returns: + json: + {"categories": + ["Jon Snow", "DanyKhaleesi69", "A Girl", ...] + "series": + [{"name": "Movies", "data": [...]} + {"name": "TV", "data": [...]}, + {"name": "Music", "data": [...]} + ] + } +``` + + +### get_plays_per_month +Get graph data by month. + +``` +Required parameters: + None + +Optional parameters: + time_range (str): The number of days of data to return + y_axis (str): "plays" or "duration" + user_id (str): The user id to filter the data + +Returns: + json: + {"categories": + ["Jan 2016", "Feb 2016", "Mar 2016", ...] + "series": + [{"name": "Movies", "data": [...]} + {"name": "TV", "data": [...]}, + {"name": "Music", "data": [...]} + ] + } +``` + + +### get_plex_log +Get the PMS logs. + +``` +Required parameters: + None + +Optional parameters: + window (int): The number of tail lines to return + log_type (str): "server" or "scanner" + +Returns: + json: + [["May 08, 2016 09:35:37", + "DEBUG", + "Auth: Came in with a super-token, authorization succeeded." + ], + [...], + [...] + ] +``` + + +### get_pms_token +Get the user's Plex token used for PlexPy. + +``` +Required parameters: + username (str): The Plex.tv username + password (str): The Plex.tv password + +Optional parameters: + None + +Returns: + string: The Plex token used for PlexPy +``` + + +### get_recently_added +Get all items that where recelty added to plex. + +``` +Required parameters: + count (str): Number of items to return + +Optional parameters: + section_id (str): The id of the Plex library section + +Returns: + json: + {"recently_added": + [{"added_at": "1461572396", + "grandparent_rating_key": "1219", + "grandparent_thumb": "/library/metadata/1219/thumb/1462175063", + "grandparent_title": "Game of Thrones", + "library_name": "", + "media_index": "1", + "media_type": "episode", + "parent_media_index": "6", + "parent_rating_key": "153036", + "parent_thumb": "/library/metadata/153036/thumb/1462175062", + "parent_title": "", + "rating_key": "153037", + "section_id": "2", + "thumb": "/library/metadata/153037/thumb/1462175060", + "title": "The Red Woman", + "year": "2016" + }, + {...}, + {...} + ] + } +``` + + +### get_server_friendly_name +Get the name of the PMS. + +``` +Required parameters: + None + +Optional parameters: + None + +Returns: + string: "Winterfell-Server" +``` + + +### get_server_id +Get the PMS server identifier. + +``` +Required parameters: + hostname (str): 'localhost' or '192.160.0.10' + port (int): 32400 + +Optional parameters: + ssl (int): 0 or 1 + remote (int): 0 or 1 + +Returns: + string: The unique PMS identifier +``` + + +### get_server_identity +Get info about the local server. + +``` +Required parameters: + None + +Optional parameters: + None + +Returns: + json: + [{"machine_identifier": "ds48g4r354a8v9byrrtr697g3g79w", + "version": "0.9.15.x.xxx-xxxxxxx" + } + ] +``` + + +### get_server_list +Get all your servers that are published to Plex.tv. + +``` +Required parameters: + None + +Optional parameters: + None + +Returns: + json: + [{"clientIdentifier": "ds48g4r354a8v9byrrtr697g3g79w", + "httpsRequired": "0", + "ip": "xxx.xxx.xxx.xxx", + "label": "Winterfell-Server", + "local": "1", + "port": "32400", + "value": "xxx.xxx.xxx.xxx" + }, + {...}, + {...} + ] +``` + + +### get_server_pref +Get a specified PMS server preference. + +``` +Required parameters: + pref (str): Name of preference + +Returns: + string: Value of preference +``` + + +### get_servers_info +Get info about the PMS. + +``` +Required parameters: + None + +Optional parameters: + None + +Returns: + json: + [{"port": "32400", + "host": "10.0.0.97", + "version": "0.9.15.2.1663-7efd046", + "name": "Winterfell-Server", + "machine_identifier": "ds48g4r354a8v9byrrtr697g3g79w" + } + ] +``` + + +### get_settings +Gets all settings from the config file. + +``` +Required parameters: + None + +Optional parameters: + key (str): Name of a config section to return + +Returns: + json: + {"General": {"api_enabled": true, ...} + "Advanced": {"cache_sizemb": "32", ...}, + ... + } +``` + + +### get_stream_type_by_top_10_platforms +Get graph data by stream type by top 10 platforms. + +``` +Required parameters: + None + +Optional parameters: + time_range (str): The number of days of data to return + y_axis (str): "plays" or "duration" + user_id (str): The user id to filter the data + +Returns: + json: + {"categories": + ["iOS", "Android", "Chrome", ...] + "series": + [{"name": "Direct Play", "data": [...]} + {"name": "Direct Stream", "data": [...]}, + {"name": "Transcode", "data": [...]} + ] + } +``` + + +### get_stream_type_by_top_10_users +Get graph data by stream type by top 10 users. + +``` +Required parameters: + None + +Optional parameters: + time_range (str): The number of days of data to return + y_axis (str): "plays" or "duration" + user_id (str): The user id to filter the data + +Returns: + json: + {"categories": + ["Jon Snow", "DanyKhaleesi69", "A Girl", ...] + "series": + [{"name": "Direct Play", "data": [...]} + {"name": "Direct Stream", "data": [...]}, + {"name": "Transcode", "data": [...]} + ] + } +``` + + +### get_synced_items +Get a list of synced items on the PMS. + +``` +Required parameters: + machine_id (str): The PMS identifier + +Optional parameters: + user_id (str): The id of the Plex user + +Returns: + json: + [{"content_type": "video", + "device_name": "Tyrion's iPad", + "failure": "", + "friendly_name": "Tyrion Lannister", + "item_complete_count": "0", + "item_count": "1", + "item_downloaded_count": "0", + "item_downloaded_percent_complete": 0, + "metadata_type": "movie", + "music_bitrate": "192", + "photo_quality": "74", + "platform": "iOS", + "rating_key": "154092", + "root_title": "Deadpool", + "state": "pending", + "sync_id": "11617019", + "title": "Deadpool", + "total_size": "0", + "user_id": "696969", + "username": "DrukenDwarfMan", + "video_quality": "60" + }, + {...}, + {...} + ] +``` + + +### get_user_ips +Get the data on PlexPy users IP table. + +``` +Required parameters: + user_id (str): The id of the Plex user + +Optional parameters: + order_column (str): "last_seen", "ip_address", "platform", "player", + "last_played", "play_count" + order_dir (str): "desc" or "asc" + start (int): Row to start from, 0 + length (int): Number of items to return, 25 + search (str): A string to search for, "xxx.xxx.xxx.xxx" + +Returns: + json: + {"draw": 1, + "recordsTotal": 2344, + "recordsFiltered": 10, + "data": + [{"friendly_name": "Jon Snow", + "id": 1121, + "ip_address": "xxx.xxx.xxx.xxx", + "last_played": "Game of Thrones - The Red Woman", + "last_seen": 1462591869, + "media_index": 1, + "media_type": "episode", + "parent_media_index": 6, + "parent_title": "", + "platform": "Chrome", + "play_count": 149, + "player": "Plex Web (Chrome)", + "rating_key": 153037, + "thumb": "/library/metadata/153036/thumb/1462175062", + "transcode_decision": "transcode", + "user_id": 133788, + "year": 2016 + }, + {...}, + {...} + ] + } +``` + + +### get_user_logins +Get the data on PlexPy user login table. + +``` +Required parameters: + user_id (str): The id of the Plex user + +Optional parameters: + order_column (str): "date", "time", "ip_address", "host", "os", "browser" + order_dir (str): "desc" or "asc" + start (int): Row to start from, 0 + length (int): Number of items to return, 25 + search (str): A string to search for, "xxx.xxx.xxx.xxx" + +Returns: + json: + {"draw": 1, + "recordsTotal": 2344, + "recordsFiltered": 10, + "data": + [{"browser": "Safari 7.0.3", + "friendly_name": "Jon Snow", + "host": "http://plexpy.castleblack.com", + "ip_address": "xxx.xxx.xxx.xxx", + "os": "Mac OS X", + "timestamp": 1462591869, + "user": "LordCommanderSnow", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A", + "user_group": "guest", + "user_id": 133788 + }, + {...}, + {...} + ] + } +``` + + +### get_user_names +Get a list of all user and user ids. + +``` +Required parameters: + None + +Optional parameters: + None + +Returns: + json: + [{"friendly_name": "Jon Snow", "user_id": 133788}, + {"friendly_name": "DanyKhaleesi69", "user_id": 8008135}, + {"friendly_name": "Tyrion Lannister", "user_id": 696969}, + {...}, + ] +``` + + +### get_users +Get a list of all users that have access to your server. + +``` +Required parameters: + None + +Optional parameters: + None + +Returns: + json: + [{"email": "Jon.Snow.1337@CastleBlack.com", + "filter_all": "", + "filter_movies": "", + "filter_music": "", + "filter_photos": "", + "filter_tv": "", + "is_allow_sync": null, + "is_home_user": "1", + "is_restricted": "0", + "thumb": "https://plex.tv/users/k10w42309cynaopq/avatar", + "user_id": "133788", + "username": "Jon Snow" + }, + {...}, + {...} + ] +``` + + +### get_users_table +Get the data on PlexPy users table. + +``` +Required parameters: + None + +Optional parameters: + order_column (str): "user_thumb", "friendly_name", "last_seen", "ip_address", "platform", + "player", "last_played", "plays", "duration" + order_dir (str): "desc" or "asc" + start (int): Row to start from, 0 + length (int): Number of items to return, 25 + search (str): A string to search for, "Jon Snow" + +Returns: + json: + {"draw": 1, + "recordsTotal": 10, + "recordsFiltered": 10, + "data": + [{"allow_guest": "Checked", + "do_notify": "Checked", + "duration": 2998290, + "friendly_name": "Jon Snow", + "id": 1121, + "ip_address": "xxx.xxx.xxx.xxx", + "keep_history": "Checked", + "last_played": "Game of Thrones - The Red Woman", + "last_seen": 1462591869, + "media_index": 1, + "media_type": "episode", + "parent_media_index": 6, + "parent_title": "", + "platform": "Chrome", + "player": "Plex Web (Chrome)", + "plays": 487, + "rating_key": 153037, + "thumb": "/library/metadata/153036/thumb/1462175062", + "transcode_decision": "transcode", + "user_id": 133788, + "user_thumb": "https://plex.tv/users/568gwwoib5t98a3a/avatar", + "year": 2016 + }, + {...}, + {...} + ] + } +``` + + +### import_database +Import a PlexWatch or Plexivity database into PlexPy. + +``` +Required parameters: + app (str): "plexwatch" or "plexivity" + database_path (str): The full path to the plexwatch database file + table_name (str): "processed" or "grouped" + +Optional parameters: + import_ignore_interval (int): The minimum number of seconds for a stream to import + +Returns: + None +``` + + +### notify +Send a notification using PlexPy. + +``` +Required parameters: + agent_id(str): The id of the notification agent to use + subject(str): The subject of the message + body(str): The body of the message + +Optional parameters: + None + +Returns: + None +``` + + +### refresh_libraries_list +Refresh the PlexPy libraries list. + + +### refresh_users_list +Refresh the PlexPy users list. -### shutdown -No params -Shut down plexpy ### restart -No params -Restart plexpy +Restart PlexPy. + + +### search +Get search results from the PMS. + +``` +Required parameters: + query (str): The query string to search for + +Returns: + json: + {"results_count": 69, + "results_list": + {"movie": + [{...}, + {...}, + ] + }, + {"episode": + [{...}, + {...}, + ] + }, + {...} + } +``` + + +### sql +Query the PlexPy database with raw SQL. Automatically makes a backup of +the database if the latest backup is older then 24h. `api_sql` must be +manually enabled in the config file. + +``` +Required parameters: + query (str): The SQL query + +Optional parameters: + None + +Returns: + None +``` + + +### undelete_library +Restore a deleted library section to PlexPy. + +``` +Required parameters: + section_id (str): The id of the Plex library section + section_name (str): The name of the Plex library section + +Optional parameters: + None + +Returns: + None +``` + + +### undelete_user +Restore a deleted user to PlexPy. + +``` +Required parameters: + user_id (str): The id of the Plex user + username (str): The username of the Plex user + +Optional parameters: + None + +Returns: + None +``` + ### update -No params -Update plexpy - you may want to check the install type in get version and not allow this if type==exe +Check for PlexPy updates on Github. + + +### update_metadata_details +Update the metadata in the PlexPy database by matching rating keys. +Also updates all parents or children of the media item if it is a show/season/episode +or artist/album/track. + +``` +Required parameters: + old_rating_key (str): 12345 + new_rating_key (str): 54321 + media_type (str): "movie", "show", "season", "episode", "artist", "album", "track" + +Optional parameters: + None + +Returns: + None +``` + diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a0a6609..95712e2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,56 @@ # Changelog +## v1.4.0 (2016-05-15) + +* New: An HTML form login page with sessions support. +* New: Guest access control for shared users using Plex.tv authentication. + * Enable the option in the settings and toggle guest access per user from Users > Edit mode. + * Guests can only view their own user data. Other user info is removed/masked. + * Guests can only view media from libraries that are shared with them (content rating and label filters are respected). Other libraries are removed/masked. + * All settings and admin controls are restricted from guests. + * All current activity on the server is shown, but with masked user/metadata info. +* New: Login logs table on the User and Logs pages. +* New: Filter the history table by user. +* New: Filter the graphs by user. (Thanks @Otger) +* New: Option to hash the admin passowrd in the config file. +* New: Options to enable/disable/rearrange each section on the homepage +* New: Toggle media types for recently added items on the homepage. +* New: Option to enter an Imgur API client ID for uploading posters. + * Note: The shared Imgur client id will be removed in a future PlexPy update! Please enter your own client id in the settings to continue uploading posters! +* New: HTML support for Email. +* New: Posters and HTML support for Telegram. +* New: Poster support for Slack. +* New: Poster support for Twitter. +* New: Re-added Plex Home Theater notification agent. +* New: Browser notification agent (experimental). +* New: Added {plex_url} as a notification option. +* New: Added transcode decision to the activity header. +* New: Documentation for APIv2 (see API.md for details). +* New: Import a Plexivity database into PlexPy. +* New: Prettier fallback image for art/episodes. +* New: Prettier confirm modal dialogues. +* New: Cache images to reduce Plex API calls. This can be disabled in the under Settings > Extra Settings. (Thanks @Hellowlol) +* New: Scheduled backups of the config file. +* New: Button to clear the PlexPy cache/images in the settings. +* New: Button to manually backup the PlexPy database/config in the settings. +* New: Button to clear the PlexPy logs in the settings. +* New: Button to download PlexPy log file on the Logs tab. +* New: Advanced setting in config file to change the Plex API timeout value. +* Fix: Mixed content HTTP request in settings (for reverse proxies with SSL). +* Fix: Rename recently "watched" music to "played". +* Change: Current activity details now persists across refreshes. +* Change: Smoother transitions between preview thumbnails in current activity. +* Change: Datatables now display all columns and scroll horizontally on smaller screens. +* Change: Ability to change the base URL for reverse proxies in the web interface. +* Change: Added a "Verify Server" button in the settings. +* Change: Added request status code in the logs for notifer errors. +* Change: Remove in-memory logs and read lines from log file instead. (Thanks @Hellowlol) +* Change: Limit number of failed attempts to write sessions to history. Default is 5 attempts. +* Change: A bunch of UI updates. +* Change: A bunch of backend code cleanup. +* Removed: All unused Python packages. + + ## v1.3.16 (2016-05-01) * Fix: Viewing photos crashing PlexPy. diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index e297ffb1..9193aa24 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -1,42 +1,41 @@ -### Reporting Issues: + -**Use proper markdown syntax to structure your post (i.e. code/log in code blocks).** +**Version:** -**Make sure you provide the following information below:** -- [ ] Version +**Branch:** + +**Commit hash:** + +**Operating system:** + +**Python version:** + +**What you did?** + +**What happened?** + +**What you expected?** + +**How can we reproduce your issue?** + + +**What are your (relevant) settings?** + +**Link to logs:** -- [ ] Branch - - -- [ ] Commit hash - - -- [ ] Operating system - - -- [ ] Python version - - -- [ ] What you did? - - -- [ ] What happened? - - -- [ ] What you expected? - - -- [ ] How can we reproduce your issue? - - -- [ ] What are your (relevant) settings? - - -- [ ] Include a link to your **FULL** (not just a few lines!) log file that has the error. Please use [Gist](http://gist.github.com) or [Pastebin](http://pastebin.com/). - + diff --git a/PlexPy.py b/PlexPy.py index b601fcee..5ea68130 100755 --- a/PlexPy.py +++ b/PlexPy.py @@ -27,13 +27,14 @@ import sys # Ensure lib added to path, before any other imports sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'lib/')) -from plexpy import webstart, logger, web_socket - -import locale -import time -import signal import argparse +import locale +import signal +import time + import plexpy +from plexpy import config, database, logger, web_socket, webstart + # Register signals, such as CTRL + C signal.signal(signal.SIGINT, plexpy.sig_handler) @@ -147,7 +148,7 @@ def main(): if args.config: config_file = args.config else: - config_file = os.path.join(plexpy.DATA_DIR, 'config.ini') + config_file = os.path.join(plexpy.DATA_DIR, config.FILENAME) # Try to create the DATA_DIR if it doesn't exist if not os.path.exists(plexpy.DATA_DIR): @@ -163,7 +164,7 @@ def main(): 'Cannot write to the data directory: ' + plexpy.DATA_DIR + '. Exiting...') # Put the database in the DATA_DIR - plexpy.DB_FILE = os.path.join(plexpy.DATA_DIR, 'plexpy.db') + plexpy.DB_FILE = os.path.join(plexpy.DATA_DIR, database.FILENAME) if plexpy.DAEMON: plexpy.daemonize() diff --git a/data/interfaces/default/plexwatch_import.html b/data/interfaces/default/app_import.html similarity index 77% rename from data/interfaces/default/plexwatch_import.html rename to data/interfaces/default/app_import.html index 1330b2b5..63e65c17 100644 --- a/data/interfaces/default/plexwatch_import.html +++ b/data/interfaces/default/app_import.html @@ -2,11 +2,18 @@
- Please ensure your PlexWatch database is at version 0.3.2 or higher. + <% + v = '' + if app == 'PlexWatch': + v = '0.3.2' + elif app == 'Plexivity': + v = '0.9.8' + %> + Please ensure your ${app} database is at version ${v} or higher.
Enter the path and file name for the PlexWatch database you wish to import.
+Enter the path and file name for the ${app} database you wish to import.
Uncheck this if you do not want to keep any history on this user's activity.
+Uncheck this if you do not want to allow this user to login to PlexPy.
+@@ -718,6 +842,7 @@ DOCUMENTATION :: END
@@ -764,6 +898,7 @@ DOCUMENTATION :: END
+
% endif
% if data['media_type'] != 'track' and data['video_resolution']:
-
+
% endif
% if data['audio_codec']:
-
+
% endif
% if data['audio_channels']:
-
+
% endif
f&&(e=0)): -"first"==b?e=0:"previous"==b?(e=0<=d?e-d:0,0>e&&(e=0)):"next"==b?e+d",{id:!a.aanFeatures.r?a.sTableId+"_processing":null,"class":a.oClasses.sProcessing}).html(a.oLanguage.sProcessing).insertBefore(a.nTable)[0]}function C(a,b){a.oFeatures.bProcessing&&h(a.aanFeatures.r).css("display",b?"block":"none");w(a, -null,"processing",[a,b])}function qb(a){var b=h(a.nTable);b.attr("role","grid");var c=a.oScroll;if(""===c.sX&&""===c.sY)return a.nTable;var e=c.sX,d=c.sY,f=a.oClasses,g=b.children("caption"),j=g.length?g[0]._captionSide:null,i=h(b[0].cloneNode(!1)),o=h(b[0].cloneNode(!1)),l=b.children("tfoot");c.sX&&"100%"===b.attr("width")&&b.removeAttr("width");l.length||(l=null);c=h("",{"class":f.sScrollWrapper}).append(h("",{"class":f.sScrollHead}).css({overflow:"hidden",position:"relative",border:0, -width:e?!e?null:s(e):"100%"}).append(h("",{"class":f.sScrollHeadInner}).css({"box-sizing":"content-box",width:c.sXInner||"100%"}).append(i.removeAttr("id").css("margin-left",0).append("top"===j?g:null).append(b.children("thead"))))).append(h("",{"class":f.sScrollBody}).css({overflow:"auto",height:!d?null:s(d),width:!e?null:s(e)}).append(b));l&&c.append(h("",{"class":f.sScrollFoot}).css({overflow:"hidden",border:0,width:e?!e?null:s(e):"100%"}).append(h("",{"class":f.sScrollFootInner}).append(o.removeAttr("id").css("margin-left", -0).append("bottom"===j?g:null).append(b.children("tfoot")))));var b=c.children(),q=b[0],f=b[1],n=l?b[2]:null;if(e)h(f).on("scroll.DT",function(){var a=this.scrollLeft;q.scrollLeft=a;l&&(n.scrollLeft=a)});a.nScrollHead=q;a.nScrollBody=f;a.nScrollFoot=n;a.aoDrawCallback.push({fn:Y,sName:"scrolling"});return c[0]}function Y(a){var b=a.oScroll,c=b.sX,e=b.sXInner,d=b.sY,f=b.iBarWidth,g=h(a.nScrollHead),j=g[0].style,i=g.children("div"),o=i[0].style,l=i.children("table"),i=a.nScrollBody,q=h(i),n=i.style, -k=h(a.nScrollFoot).children("div"),p=k.children("table"),m=h(a.nTHead),r=h(a.nTable),t=r[0],O=t.style,L=a.nTFoot?h(a.nTFoot):null,ha=a.oBrowser,w=ha.bScrollOversize,v,u,y,x,z,A=[],B=[],C=[],D,E=function(a){a=a.style;a.paddingTop="0";a.paddingBottom="0";a.borderTopWidth="0";a.borderBottomWidth="0";a.height=0};r.children("thead, tfoot").remove();z=m.clone().prependTo(r);v=m.find("tr");y=z.find("tr");z.find("th, td").removeAttr("tabindex");L&&(x=L.clone().prependTo(r),u=L.find("tr"),x=x.find("tr")); -c||(n.width="100%",g[0].style.width="100%");h.each(qa(a,z),function(b,c){D=la(a,b);c.style.width=a.aoColumns[D].sWidth});L&&G(function(a){a.style.width=""},x);b.bCollapse&&""!==d&&(n.height=q[0].offsetHeight+m[0].offsetHeight+"px");g=r.outerWidth();if(""===c){if(O.width="100%",w&&(r.find("tbody").height()>i.offsetHeight||"scroll"==q.css("overflow-y")))O.width=s(r.outerWidth()-f)}else""!==e?O.width=s(e):g==q.width()&&q.height() g-f&&(O.width=s(g))):O.width= -s(g);g=r.outerWidth();G(E,y);G(function(a){C.push(a.innerHTML);A.push(s(h(a).css("width")))},y);G(function(a,b){a.style.width=A[b]},v);h(y).height(0);L&&(G(E,x),G(function(a){B.push(s(h(a).css("width")))},x),G(function(a,b){a.style.width=B[b]},u),h(x).height(0));G(function(a,b){a.innerHTML=' '+C[b]+"";a.style.width=A[b]},y);L&&G(function(a,b){a.innerHTML="";a.style.width=B[b]},x);if(r.outerWidth()i.offsetHeight|| -"scroll"==q.css("overflow-y")?g+f:g;if(w&&(i.scrollHeight>i.offsetHeight||"scroll"==q.css("overflow-y")))O.width=s(u-f);(""===c||""!==e)&&I(a,1,"Possible column misalignment",6)}else u="100%";n.width=s(u);j.width=s(u);L&&(a.nScrollFoot.style.width=s(u));!d&&w&&(n.height=s(t.offsetHeight+f));d&&b.bCollapse&&(n.height=s(d),b=c&&t.offsetWidth>i.offsetWidth?f:0,t.offsetHeight i.clientHeight|| -"scroll"==q.css("overflow-y");ha="padding"+(ha.bScrollbarLeft?"Left":"Right");o[ha]=l?f+"px":"0px";L&&(p[0].style.width=s(b),k[0].style.width=s(b),k[0].style[ha]=l?f+"px":"0px");q.scroll();if((a.bSorted||a.bFiltered)&&!a._drawHold)i.scrollTop=0}function G(a,b,c){for(var e=0,d=0,f=b.length,g,j;d ").appendTo(j.find("tbody"));j.find("tfoot th, tfoot td").css("width", -"");i=qa(a,j.find("thead")[0]);for(n=0;n ").css("width",s(a)).appendTo(b||Q.body),e=c[0].offsetWidth;c.remove();return e}function Fb(a,b){var c=a.oScroll;if(c.sX||c.sY)c=!c.sX?c.iBarWidth:0,b.style.width=s(h(b).outerWidth()-c)}function Eb(a,b){var c=Gb(a,b);if(0>c)return null;var e=a.aoData[c];return!e.nTr?h(" ").html(x(a,c,b,"display"))[0]:e.anCells[b]}function Gb(a,b){for(var c,e=-1,d=-1,f=0,g=a.aoData.length;f e&&(e=c.length,d=f);return d}function s(a){return null===a?"0px":"number"==typeof a?0>a?"0px":a+"px":a.match(/\d$/)?a+"px":a}function Hb(){var a=m.__scrollbarWidth;if(a===k){var b=h("").css({position:"absolute",top:0,left:0,width:"100%",height:150,padding:0,overflow:"scroll",visibility:"hidden"}).appendTo("body"),a=b[0].offsetWidth-b[0].clientWidth;m.__scrollbarWidth=a;b.remove()}return a}function U(a){var b,c,e=[],d=a.aoColumns,f,g,j,i;b=a.aaSortingFixed;c=h.isPlainObject(b);var o=[]; -f=function(a){a.length&&!h.isArray(a[0])?o.push(a):o.push.apply(o,a)};h.isArray(b)&&f(b);c&&b.pre&&f(b.pre);f(a.aaSorting);c&&b.post&&f(b.post);for(a=0;a d?1:0,0!==c)return"asc"===j.dir?c:-c;c=e[a];d=e[b];return c d?1:0}):i.sort(function(a,b){var c,g,j,i,k=h.length,m=f[a]._aSortData,r=f[b]._aSortData;for(j=0;j g?1:0})}a.bSorted=!0}function Jb(a){for(var b,c,e=a.aoColumns,d=U(a),a=a.oLanguage.oAria,f=0,g=e.length;f /g,"");var i=c.nTh;i.removeAttribute("aria-sort");c.bSortable&&(0 d?d+1:3));d=0;for(f=e.length;d d?d+1:3))}a.aLastSort=e}function Ib(a,b){var c=a.aoColumns[b],e=m.ext.order[c.sSortDataType],d;e&&(d=e.call(a.oInstance,a,b,$(a,b)));for(var f,g=m.ext.type.order[c.sType+"-pre"],j=0,i=a.aoData.length;j=e.length?[0,c[1]]:c)}));d.search!==k&&h.extend(a.oPreviousSearch,Ab(d.search));b=0;for(c=d.columns.length;b =c&&(b=c-e);b-=b%e;if(-1===e||0>b)b=0;a._iDisplayStart=b}function Pa(a,b){var c=a.renderer,e=m.ext.renderer[b];return h.isPlainObject(c)&&c[b]?e[c[b]]||e._:"string"===typeof c?e[c]||e._:e._}function B(a){return a.oFeatures.bServerSide? -"ssp":a.ajax||a.sAjaxSource?"ajax":"dom"}function Wa(a,b){var c=[],c=Mb.numbers_length,e=Math.floor(c/2);b<=c?c=V(0,b):a<=e?(c=V(0,c-2),c.push("ellipsis"),c.push(b-1)):(a>=b-1-e?c=V(b-(c-2),b):(c=V(a-e+2,a+e-1),c.push("ellipsis"),c.push(b-1)),c.splice(0,0,"ellipsis"),c.splice(0,0,0));c.DT_el="span";return c}function db(a){h.each({num:function(b){return Aa(b,a)},"num-fmt":function(b){return Aa(b,a,Xa)},"html-num":function(b){return Aa(b,a,Ba)},"html-num-fmt":function(b){return Aa(b,a,Ba,Xa)}},function(b, -c){u.type.order[b+a+"-pre"]=c;b.match(/^html\-/)&&(u.type.search[b+a]=u.type.search.html)})}function Nb(a){return function(){var b=[za(this[m.ext.iApiIndex])].concat(Array.prototype.slice.call(arguments));return m.ext.internal[a].apply(this,b)}}var m,u,t,r,v,Ya={},Ob=/[\r\n]/g,Ba=/<.*?>/g,ac=/^[\w\+\-]/,bc=/[\w\+\-]$/,Yb=RegExp("(\\/|\\.|\\*|\\+|\\?|\\||\\(|\\)|\\[|\\]|\\{|\\}|\\\\|\\$|\\^|\\-)","g"),Xa=/[',$\u00a3\u20ac\u00a5%\u2009\u202F\u20BD\u20a9\u20BArfk]/gi,J=function(a){return!a||!0===a|| -"-"===a?!0:!1},Pb=function(a){var b=parseInt(a,10);return!isNaN(b)&&isFinite(a)?b:null},Qb=function(a,b){Ya[b]||(Ya[b]=RegExp(va(b),"g"));return"string"===typeof a&&"."!==b?a.replace(/\./g,"").replace(Ya[b],"."):a},Za=function(a,b,c){var e="string"===typeof a;if(J(a))return!0;b&&e&&(a=Qb(a,b));c&&e&&(a=a.replace(Xa,""));return!isNaN(parseFloat(a))&&isFinite(a)},Rb=function(a,b,c){return J(a)?!0:!(J(a)||"string"===typeof a)?null:Za(a.replace(Ba,""),b,c)?!0:null},D=function(a,b,c){var e=[],d=0,f=a.length; -if(c!==k)for(;d ")[0],Zb=wa.textContent!==k,$b=/<.*?>/g;m=function(a){this.$=function(a,b){return this.api(!0).$(a,b)};this._=function(a,b){return this.api(!0).rows(a,b).data()};this.api=function(a){return a?new t(za(this[u.iApiIndex])):new t(this)};this.fnAddData=function(a,b){var c=this.api(!0),e=h.isArray(a)&&(h.isArray(a[0])||h.isPlainObject(a[0]))?c.rows.add(a):c.row.add(a);(b=== -k||b)&&c.draw();return e.flatten().toArray()};this.fnAdjustColumnSizing=function(a){var b=this.api(!0).columns.adjust(),c=b.settings()[0],e=c.oScroll;a===k||a?b.draw(!1):(""!==e.sX||""!==e.sY)&&Y(c)};this.fnClearTable=function(a){var b=this.api(!0).clear();(a===k||a)&&b.draw()};this.fnClose=function(a){this.api(!0).row(a).child.hide()};this.fnDeleteRow=function(a,b,c){var e=this.api(!0),a=e.rows(a),d=a.settings()[0],h=d.aoData[a[0][0]];a.remove();b&&b.call(this,d,h);(c===k||c)&&e.draw();return h}; -this.fnDestroy=function(a){this.api(!0).destroy(a)};this.fnDraw=function(a){this.api(!0).draw(a)};this.fnFilter=function(a,b,c,e,d,h){d=this.api(!0);null===b||b===k?d.search(a,c,e,h):d.column(b).search(a,c,e,h);d.draw()};this.fnGetData=function(a,b){var c=this.api(!0);if(a!==k){var e=a.nodeName?a.nodeName.toLowerCase():"";return b!==k||"td"==e||"th"==e?c.cell(a,b).data():c.row(a).data()||null}return c.data().toArray()};this.fnGetNodes=function(a){var b=this.api(!0);return a!==k?b.row(a).node():b.rows().nodes().flatten().toArray()}; -this.fnGetPosition=function(a){var b=this.api(!0),c=a.nodeName.toUpperCase();return"TR"==c?b.row(a).index():"TD"==c||"TH"==c?(a=b.cell(a).index(),[a.row,a.columnVisible,a.column]):null};this.fnIsOpen=function(a){return this.api(!0).row(a).child.isShown()};this.fnOpen=function(a,b,c){return this.api(!0).row(a).child(b,c).show().child()[0]};this.fnPageChange=function(a,b){var c=this.api(!0).page(a);(b===k||b)&&c.draw(!1)};this.fnSetColumnVis=function(a,b,c){a=this.api(!0).column(a).visible(b);(c=== -k||c)&&a.columns.adjust().draw()};this.fnSettings=function(){return za(this[u.iApiIndex])};this.fnSort=function(a){this.api(!0).order(a).draw()};this.fnSortListener=function(a,b,c){this.api(!0).order.listener(a,b,c)};this.fnUpdate=function(a,b,c,e,d){var h=this.api(!0);c===k||null===c?h.row(b).data(a):h.cell(b,c).data(a);(d===k||d)&&h.columns.adjust();(e===k||e)&&h.draw();return 0};this.fnVersionCheck=u.fnVersionCheck;var b=this,c=a===k,e=this.length;c&&(a={});this.oApi=this.internal=u.internal;for(var d in m.ext.internal)d&& -(this[d]=Nb(d));this.each(function(){var d={},d=1 t<"F"ip>'),p.renderer)? -h.isPlainObject(p.renderer)&&!p.renderer.header&&(p.renderer.header="jqueryui"):p.renderer="jqueryui":h.extend(i,m.ext.classes,d.oClasses);q.addClass(i.sTable);if(""!==p.oScroll.sX||""!==p.oScroll.sY)p.oScroll.iBarWidth=Hb();!0===p.oScroll.sX&&(p.oScroll.sX="100%");p.iInitDisplayStart===k&&(p.iInitDisplayStart=d.iDisplayStart,p._iDisplayStart=d.iDisplayStart);null!==d.iDeferLoading&&(p.bDeferLoading=!0,g=h.isArray(d.iDeferLoading),p._iRecordsDisplay=g?d.iDeferLoading[0]:d.iDeferLoading,p._iRecordsTotal= -g?d.iDeferLoading[1]:d.iDeferLoading);var t=p.oLanguage;h.extend(!0,t,d.oLanguage);""!==t.sUrl&&(h.ajax({dataType:"json",url:t.sUrl,success:function(a){P(a);H(l.oLanguage,a);h.extend(true,t,a);ga(p)},error:function(){ga(p)}}),o=!0);null===d.asStripeClasses&&(p.asStripeClasses=[i.sStripeOdd,i.sStripeEven]);var g=p.asStripeClasses,s=q.children("tbody").find("tr").eq(0);-1!==h.inArray(!0,h.map(g,function(a){return s.hasClass(a)}))&&(h("tbody tr",this).removeClass(g.join(" ")),p.asDestroyStripes=g.slice()); -n=[];g=this.getElementsByTagName("thead");0!==g.length&&(da(p.aoHeader,g[0]),n=qa(p));if(null===d.aoColumns){r=[];g=0;for(j=n.length;g ").appendTo(this));p.nTHead=j[0];j=q.children("tbody");0===j.length&&(j=h("").appendTo(this));p.nTBody=j[0];j=q.children("tfoot");if(0===j.length&&0 ").appendTo(this);0===j.length||0===j.children().length?q.addClass(i.sNoFooter): -0 a?new t(b[a],this[a]):null},filter:function(a){var b=[];if(y.filter)b=y.filter.call(this,a,this);else for(var c=0,e=this.length;c ").addClass(b), -h("td",c).addClass(b).html(a)[0].colSpan=aa(e),d.push(c[0]))};f(a,b);c._details&&c._details.remove();c._details=h(d);c._detailsShow&&c._details.insertAfter(c.nTr)}return this});r(["row().child.show()","row().child().show()"],function(){Vb(this,!0);return this});r(["row().child.hide()","row().child().hide()"],function(){Vb(this,!1);return this});r(["row().child.remove()","row().child().remove()"],function(){cb(this);return this});r("row().child.isShown()",function(){var a=this.context;return a.length&& -this.length?a[0].aoData[this[0]]._detailsShow||!1:!1});var dc=/^(.+):(name|visIdx|visible)$/,Wb=function(a,b,c,e,d){for(var c=[],e=0,f=d.length;e =0?b:g.length+b];if(typeof a==="function"){var d=Ca(c, -f);return h.map(g,function(b,f){return a(f,Wb(c,f,0,0,d),i[f])?f:null})}var k=typeof a==="string"?a.match(dc):"";if(k)switch(k[2]){case "visIdx":case "visible":b=parseInt(k[1],10);if(b<0){var m=h.map(g,function(a,b){return a.bVisible?b:null});return[m[m.length+b]]}return[la(c,b)];case "name":return h.map(j,function(a,b){return a===k[1]?b:null})}else return h(i).filter(a).map(function(){return h.inArray(this,i)}).toArray()},c,f)},1);c.selector.cols=a;c.selector.opts=b;return c});v("columns().header()", -"column().header()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].nTh},1)});v("columns().footer()","column().footer()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].nTf},1)});v("columns().data()","column().data()",function(){return this.iterator("column-rows",Wb,1)});v("columns().dataSrc()","column().dataSrc()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].mData},1)});v("columns().cache()","column().cache()", -function(a){return this.iterator("column-rows",function(b,c,e,d,f){return ia(b.aoData,f,"search"===a?"_aFilterData":"_aSortData",c)},1)});v("columns().nodes()","column().nodes()",function(){return this.iterator("column-rows",function(a,b,c,e,d){return ia(a.aoData,d,"anCells",b)},1)});v("columns().visible()","column().visible()",function(a,b){return this.iterator("column",function(c,e){if(a===k)return c.aoColumns[e].bVisible;var d=c.aoColumns,f=d[e],g=c.aoData,j,i,m;if(a!==k&&f.bVisible!==a){if(a){var l= -h.inArray(!0,D(d,"bVisible"),e+1);j=0;for(i=g.length;je;return!0};m.isDataTable=m.fnIsDataTable=function(a){var b=h(a).get(0),c=!1;h.each(m.settings, -function(a,d){var f=d.nScrollHead?h("table",d.nScrollHead)[0]:null,g=d.nScrollFoot?h("table",d.nScrollFoot)[0]:null;if(d.nTable===b||f===b||g===b)c=!0});return c};m.tables=m.fnTables=function(a){return h.map(m.settings,function(b){if(!a||a&&h(b.nTable).is(":visible"))return b.nTable})};m.util={throttle:ua,escapeRegex:va};m.camelToHungarian=H;r("$()",function(a,b){var c=this.rows(b).nodes(),c=h(c);return h([].concat(c.filter(a).toArray(),c.find(a).toArray()))});h.each(["on","one","off"],function(a, -b){r(b+"()",function(){var a=Array.prototype.slice.call(arguments);a[0].match(/\.dt\b/)||(a[0]+=".dt");var e=h(this.tables().nodes());e[b].apply(e,a);return this})});r("clear()",function(){return this.iterator("table",function(a){oa(a)})});r("settings()",function(){return new t(this.context,this.context)});r("init()",function(){var a=this.context;return a.length?a[0].oInit:null});r("data()",function(){return this.iterator("table",function(a){return D(a.aoData,"_aData")}).flatten()});r("destroy()", -function(a){a=a||!1;return this.iterator("table",function(b){var c=b.nTableWrapper.parentNode,e=b.oClasses,d=b.nTable,f=b.nTBody,g=b.nTHead,j=b.nTFoot,i=h(d),f=h(f),k=h(b.nTableWrapper),l=h.map(b.aoData,function(a){return a.nTr}),q;b.bDestroying=!0;w(b,"aoDestroyCallback","destroy",[b]);a||(new t(b)).columns().visible(!0);k.unbind(".DT").find(":not(tbody *)").unbind(".DT");h(Ea).unbind(".DT-"+b.sInstance);d!=g.parentNode&&(i.children("thead").detach(),i.append(g));j&&d!=j.parentNode&&(i.children("tfoot").detach(), -i.append(j));i.detach();k.detach();b.aaSorting=[];b.aaSortingFixed=[];xa(b);h(l).removeClass(b.asStripeClasses.join(" "));h("th, td",g).removeClass(e.sSortable+" "+e.sSortableAsc+" "+e.sSortableDesc+" "+e.sSortableNone);b.bJUI&&(h("th span."+e.sSortIcon+", td span."+e.sSortIcon,g).detach(),h("th, td",g).each(function(){var a=h("div."+e.sSortJUIWrapper,this);h(this).append(a.contents());a.detach()}));!a&&c&&c.insertBefore(d,b.nTableReinsertBefore);f.children().detach();f.append(l);i.css("width",b.sDestroyWidth).removeClass(e.sTable); -(q=b.asDestroyStripes.length)&&f.children().each(function(a){h(this).addClass(b.asDestroyStripes[a%q])});c=h.inArray(b,m.settings);-1!==c&&m.settings.splice(c,1)})});h.each(["column","row","cell"],function(a,b){r(b+"s().every()",function(a){return this.iterator(b,function(e,d,f){a.call((new t(e))[b](d,f))})})});r("i18n()",function(a,b,c){var e=this.context[0],a=R(a)(e.oLanguage);a===k&&(a=b);c!==k&&h.isPlainObject(a)&&(a=a[c]!==k?a[c]:a._);return a.replace("%d",c)});m.version="1.10.7";m.settings= -[];m.models={};m.models.oSearch={bCaseInsensitive:!0,sSearch:"",bRegex:!1,bSmart:!0};m.models.oRow={nTr:null,anCells:null,_aData:[],_aSortData:null,_aFilterData:null,_sFilterRow:null,_sRowStripe:"",src:null};m.models.oColumn={idx:null,aDataSort:null,asSorting:null,bSearchable:null,bSortable:null,bVisible:null,_sManualType:null,_bAttrSrc:!1,fnCreatedCell:null,fnGetData:null,fnSetData:null,mData:null,mRender:null,nTh:null,nTf:null,sClass:null,sContentPadding:null,sDefaultContent:null,sName:null,sSortDataType:"std", -sSortingClass:null,sSortingClassJUI:null,sTitle:null,sType:null,sWidth:null,sWidthOrig:null};m.defaults={aaData:null,aaSorting:[[0,"asc"]],aaSortingFixed:[],ajax:null,aLengthMenu:[10,25,50,100],aoColumns:null,aoColumnDefs:null,aoSearchCols:[],asStripeClasses:null,bAutoWidth:!0,bDeferRender:!1,bDestroy:!1,bFilter:!0,bInfo:!0,bJQueryUI:!1,bLengthChange:!0,bPaginate:!0,bProcessing:!1,bRetrieve:!1,bScrollCollapse:!1,bServerSide:!1,bSort:!0,bSortMulti:!0,bSortCellsTop:!1,bSortClasses:!0,bStateSave:!1, -fnCreatedRow:null,fnDrawCallback:null,fnFooterCallback:null,fnFormatNumber:function(a){return a.toString().replace(/\B(?=(\d{3})+(?!\d))/g,this.oLanguage.sThousands)},fnHeaderCallback:null,fnInfoCallback:null,fnInitComplete:null,fnPreDrawCallback:null,fnRowCallback:null,fnServerData:null,fnServerParams:null,fnStateLoadCallback:function(a){try{return JSON.parse((-1===a.iStateDuration?sessionStorage:localStorage).getItem("DataTables_"+a.sInstance+"_"+location.pathname))}catch(b){}},fnStateLoadParams:null, -fnStateLoaded:null,fnStateSaveCallback:function(a,b){try{(-1===a.iStateDuration?sessionStorage:localStorage).setItem("DataTables_"+a.sInstance+"_"+location.pathname,JSON.stringify(b))}catch(c){}},fnStateSaveParams:null,iStateDuration:7200,iDeferLoading:null,iDisplayLength:10,iDisplayStart:0,iTabIndex:0,oClasses:{},oLanguage:{oAria:{sSortAscending:": activate to sort column ascending",sSortDescending:": activate to sort column descending"},oPaginate:{sFirst:"First",sLast:"Last",sNext:"Next",sPrevious:"Previous"}, -sEmptyTable:"No data available in table",sInfo:"Showing _START_ to _END_ of _TOTAL_ entries",sInfoEmpty:"Showing 0 to 0 of 0 entries",sInfoFiltered:"(filtered from _MAX_ total entries)",sInfoPostFix:"",sDecimal:"",sThousands:",",sLengthMenu:"Show _MENU_ entries",sLoadingRecords:"Loading...",sProcessing:"Processing...",sSearch:"Search:",sSearchPlaceholder:"",sUrl:"",sZeroRecords:"No matching records found"},oSearch:h.extend({},m.models.oSearch),sAjaxDataProp:"data",sAjaxSource:null,sDom:"lfrtip",searchDelay:null, -sPaginationType:"simple_numbers",sScrollX:"",sScrollXInner:"",sScrollY:"",sServerMethod:"GET",renderer:null};W(m.defaults);m.defaults.column={aDataSort:null,iDataSort:-1,asSorting:["asc","desc"],bSearchable:!0,bSortable:!0,bVisible:!0,fnCreatedCell:null,mData:null,mRender:null,sCellType:"td",sClass:"",sContentPadding:"",sDefaultContent:null,sName:"",sSortDataType:"std",sTitle:null,sType:null,sWidth:null};W(m.defaults.column);m.models.oSettings={oFeatures:{bAutoWidth:null,bDeferRender:null,bFilter:null, -bInfo:null,bLengthChange:null,bPaginate:null,bProcessing:null,bServerSide:null,bSort:null,bSortMulti:null,bSortClasses:null,bStateSave:null},oScroll:{bCollapse:null,iBarWidth:0,sX:null,sXInner:null,sY:null},oLanguage:{fnInfoCallback:null},oBrowser:{bScrollOversize:!1,bScrollbarLeft:!1},ajax:null,aanFeatures:[],aoData:[],aiDisplay:[],aiDisplayMaster:[],aoColumns:[],aoHeader:[],aoFooter:[],oPreviousSearch:{},aoPreSearchCols:[],aaSorting:null,aaSortingFixed:[],asStripeClasses:null,asDestroyStripes:[], -sDestroyWidth:0,aoRowCallback:[],aoHeaderCallback:[],aoFooterCallback:[],aoDrawCallback:[],aoRowCreatedCallback:[],aoPreDrawCallback:[],aoInitComplete:[],aoStateSaveParams:[],aoStateLoadParams:[],aoStateLoaded:[],sTableId:"",nTable:null,nTHead:null,nTFoot:null,nTBody:null,nTableWrapper:null,bDeferLoading:!1,bInitialised:!1,aoOpenRows:[],sDom:null,searchDelay:null,sPaginationType:"two_button",iStateDuration:0,aoStateSave:[],aoStateLoad:[],oSavedState:null,oLoadedState:null,sAjaxSource:null,sAjaxDataProp:null, -bAjaxDataGet:!0,jqXHR:null,json:k,oAjaxData:k,fnServerData:null,aoServerParams:[],sServerMethod:null,fnFormatNumber:null,aLengthMenu:null,iDraw:0,bDrawing:!1,iDrawError:-1,_iDisplayLength:10,_iDisplayStart:0,_iRecordsTotal:0,_iRecordsDisplay:0,bJUI:null,oClasses:{},bFiltered:!1,bSorted:!1,bSortCellsTop:null,oInit:null,aoDestroyCallback:[],fnRecordsTotal:function(){return"ssp"==B(this)?1*this._iRecordsTotal:this.aiDisplayMaster.length},fnRecordsDisplay:function(){return"ssp"==B(this)?1*this._iRecordsDisplay: -this.aiDisplay.length},fnDisplayEnd:function(){var a=this._iDisplayLength,b=this._iDisplayStart,c=b+a,e=this.aiDisplay.length,d=this.oFeatures,f=d.bPaginate;return d.bServerSide?!1===f||-1===a?b+e:Math.min(b+a,this._iRecordsDisplay):!f||c>e||-1===a?e:c},oInstance:null,sInstance:null,iTabIndex:0,nScrollHead:null,nScrollFoot:null,aLastSort:[],oPlugins:{}};m.ext=u={buttons:{},classes:{},errMode:"alert",feature:[],search:[],selector:{cell:[],column:[],row:[]},internal:{},legacy:{ajax:null},pager:{},renderer:{pageButton:{}, -header:{}},order:{},type:{detect:[],search:{},order:{}},_unique:0,fnVersionCheck:m.fnVersionCheck,iApiIndex:0,oJUIClasses:{},sVersion:m.version};h.extend(u,{afnFiltering:u.search,aTypes:u.type.detect,ofnSearch:u.type.search,oSort:u.type.order,afnSortData:u.order,aoFeatures:u.feature,oApi:u.internal,oStdClasses:u.classes,oPagination:u.pager});h.extend(m.ext.classes,{sTable:"dataTable",sNoFooter:"no-footer",sPageButton:"paginate_button",sPageButtonActive:"current",sPageButtonDisabled:"disabled",sStripeOdd:"odd", +/*! + DataTables 1.10.11 + ©2008-2015 SpryMedia Ltd - datatables.net/license +*/ +(function(h){"function"===typeof define&&define.amd?define(["jquery"],function(D){return h(D,window,document)}):"object"===typeof exports?module.exports=function(D,I){D||(D=window);I||(I="undefined"!==typeof window?require("jquery"):require("jquery")(D));return h(I,D,D.document)}:h(jQuery,window,document)})(function(h,D,I,k){function Y(a){var b,c,d={};h.each(a,function(e){if((b=e.match(/^([^A-Z]+?)([A-Z])/))&&-1!=="a aa ai ao as b fn i m o s ".indexOf(b[1]+" "))c=e.replace(b[0],b[2].toLowerCase()), +d[c]=e,"o"===b[1]&&Y(a[e])});a._hungarianMap=d}function K(a,b,c){a._hungarianMap||Y(a);var d;h.each(b,function(e){d=a._hungarianMap[e];if(d!==k&&(c||b[d]===k))"o"===d.charAt(0)?(b[d]||(b[d]={}),h.extend(!0,b[d],b[e]),K(a[d],b[d],c)):b[d]=b[e]})}function Fa(a){var b=m.defaults.oLanguage,c=a.sZeroRecords;!a.sEmptyTable&&(c&&"No data available in table"===b.sEmptyTable)&&E(a,a,"sZeroRecords","sEmptyTable");!a.sLoadingRecords&&(c&&"Loading..."===b.sLoadingRecords)&&E(a,a,"sZeroRecords","sLoadingRecords"); +a.sInfoThousands&&(a.sThousands=a.sInfoThousands);(a=a.sDecimal)&&db(a)}function eb(a){A(a,"ordering","bSort");A(a,"orderMulti","bSortMulti");A(a,"orderClasses","bSortClasses");A(a,"orderCellsTop","bSortCellsTop");A(a,"order","aaSorting");A(a,"orderFixed","aaSortingFixed");A(a,"paging","bPaginate");A(a,"pagingType","sPaginationType");A(a,"pageLength","iDisplayLength");A(a,"searching","bFilter");"boolean"===typeof a.sScrollX&&(a.sScrollX=a.sScrollX?"100%":"");"boolean"===typeof a.scrollX&&(a.scrollX= +a.scrollX?"100%":"");if(a=a.aoSearchCols)for(var b=0,c=a.length;b ").css({position:"fixed",top:0,left:0,height:1,width:1,overflow:"hidden"}).append(h("").css({position:"absolute",top:1,left:1, +width:100,overflow:"scroll"}).append(h("").css({width:"100%",height:10}))).appendTo("body"),d=c.children(),e=d.children();b.barWidth=d[0].offsetWidth-d[0].clientWidth;b.bScrollOversize=100===e[0].offsetWidth&&100!==d[0].clientWidth;b.bScrollbarLeft=1!==Math.round(e.offset().left);b.bBounding=c[0].getBoundingClientRect().width?!0:!1;c.remove()}h.extend(a.oBrowser,m.__browser);a.oScroll.iBarWidth=m.__browser.barWidth}function hb(a,b,c,d,e,f){var g,j=!1;c!==k&&(g=c,j=!0);for(;d!==e;)a.hasOwnProperty(d)&& +(g=j?b(g,a[d],d,a):a[d],j=!0,d+=f);return g}function Ga(a,b){var c=m.defaults.column,d=a.aoColumns.length,c=h.extend({},m.models.oColumn,c,{nTh:b?b:I.createElement("th"),sTitle:c.sTitle?c.sTitle:b?b.innerHTML:"",aDataSort:c.aDataSort?c.aDataSort:[d],mData:c.mData?c.mData:d,idx:d});a.aoColumns.push(c);c=a.aoPreSearchCols;c[d]=h.extend({},m.models.oSearch,c[d]);ja(a,d,h(b).data())}function ja(a,b,c){var b=a.aoColumns[b],d=a.oClasses,e=h(b.nTh);if(!b.sWidthOrig){b.sWidthOrig=e.attr("width")||null;var f= +(e.attr("style")||"").match(/width:\s*(\d+[pxem%]+)/);f&&(b.sWidthOrig=f[1])}c!==k&&null!==c&&(fb(c),K(m.defaults.column,c),c.mDataProp!==k&&!c.mData&&(c.mData=c.mDataProp),c.sType&&(b._sManualType=c.sType),c.className&&!c.sClass&&(c.sClass=c.className),h.extend(b,c),E(b,c,"sWidth","sWidthOrig"),c.iDataSort!==k&&(b.aDataSort=[c.iDataSort]),E(b,c,"aDataSort"));var g=b.mData,j=Q(g),i=b.mRender?Q(b.mRender):null,c=function(a){return"string"===typeof a&&-1!==a.indexOf("@")};b._bAttrSrc=h.isPlainObject(g)&& +(c(g.sort)||c(g.type)||c(g.filter));b._setter=null;b.fnGetData=function(a,b,c){var d=j(a,b,k,c);return i&&b?i(d,b,a,c):d};b.fnSetData=function(a,b,c){return R(g)(a,b,c)};"number"!==typeof g&&(a._rowReadObject=!0);a.oFeatures.bSort||(b.bSortable=!1,e.addClass(d.sSortableNone));a=-1!==h.inArray("asc",b.asSorting);c=-1!==h.inArray("desc",b.asSorting);!b.bSortable||!a&&!c?(b.sSortingClass=d.sSortableNone,b.sSortingClassJUI=""):a&&!c?(b.sSortingClass=d.sSortableAsc,b.sSortingClassJUI=d.sSortJUIAscAllowed): +!a&&c?(b.sSortingClass=d.sSortableDesc,b.sSortingClassJUI=d.sSortJUIDescAllowed):(b.sSortingClass=d.sSortable,b.sSortingClassJUI=d.sSortJUI)}function U(a){if(!1!==a.oFeatures.bAutoWidth){var b=a.aoColumns;Ha(a);for(var c=0,d=b.length;c q[f])d(l.length+q[f],n);else if("string"===typeof q[f]){j=0;for(i=l.length;jb&&a[e]--; -1!=d&&c===k&&a.splice(d,1)}function ca(a,b,c,d){var e=a.aoData[b],f,g=function(c,d){for(;c.childNodes.length;)c.removeChild(c.firstChild); +c.innerHTML=B(a,b,d,"display")};if("dom"===c||(!c||"auto"===c)&&"dom"===e.src)e._aData=Ka(a,e,d,d===k?k:e._aData).data;else{var j=e.anCells;if(j)if(d!==k)g(j[d],d);else{c=0;for(f=j.length;c ").appendTo(g));b=0;for(c=l.length;b tr").attr("role","row");h(g).find(">tr>th, >tr>td").addClass(n.sHeaderTH);h(j).find(">tr>th, >tr>td").addClass(n.sFooterTH); +if(null!==j){a=a.aoFooter[0];b=0;for(c=a.length;b =a.fnRecordsDisplay()?0:g,a.iInitDisplayStart= +-1);var g=a._iDisplayStart,n=a.fnDisplayEnd();if(a.bDeferLoading)a.bDeferLoading=!1,a.iDraw++,C(a,!1);else if(j){if(!a.bDestroying&&!lb(a))return}else a.iDraw++;if(0!==i.length){f=j?a.aoData.length:n;for(j=j?0:g;j ",{"class":e?d[0]:""}).append(h(" ",{valign:"top",colSpan:aa(a),"class":a.oClasses.sRowEmpty}).html(c))[0];u(a,"aoHeaderCallback","header",[h(a.nTHead).children("tr")[0],Ma(a),g,n,i]);u(a,"aoFooterCallback","footer",[h(a.nTFoot).children("tr")[0],Ma(a),g,n,i]);d=h(a.nTBody);d.children().detach();d.append(h(b));u(a,"aoDrawCallback","draw",[a]);a.bSorted=!1;a.bFiltered=!1;a.bDrawing=!1}}function T(a,b){var c=a.oFeatures,d=c.bFilter; +c.bSort&&mb(a);d?fa(a,a.oPreviousSearch):a.aiDisplay=a.aiDisplayMaster.slice();!0!==b&&(a._iDisplayStart=0);a._drawHold=b;O(a);a._drawHold=!1}function nb(a){var b=a.oClasses,c=h(a.nTable),c=h("").insertBefore(c),d=a.oFeatures,e=h("",{id:a.sTableId+"_wrapper","class":b.sWrapper+(a.nTFoot?"":" "+b.sNoFooter)});a.nHolding=c[0];a.nTableWrapper=e[0];a.nTableReinsertBefore=a.nTable.nextSibling;for(var f=a.sDom.split(""),g,j,i,n,l,q,t=0;t ")[0]; +n=f[t+1];if("'"==n||'"'==n){l="";for(q=2;f[t+q]!=n;)l+=f[t+q],q++;"H"==l?l=b.sJUIHeader:"F"==l&&(l=b.sJUIFooter);-1!=l.indexOf(".")?(n=l.split("."),i.id=n[0].substr(1,n[0].length-1),i.className=n[1]):"#"==l.charAt(0)?i.id=l.substr(1,l.length-1):i.className=l;t+=q}e.append(i);e=h(i)}else if(">"==j)e=e.parent();else if("l"==j&&d.bPaginate&&d.bLengthChange)g=ob(a);else if("f"==j&&d.bFilter)g=pb(a);else if("r"==j&&d.bProcessing)g=qb(a);else if("t"==j)g=rb(a);else if("i"==j&&d.bInfo)g=sb(a);else if("p"== +j&&d.bPaginate)g=tb(a);else if(0!==m.ext.feature.length){i=m.ext.feature;q=0;for(n=i.length;q ',j=d.sSearch,j=j.match(/_INPUT_/)?j.replace("_INPUT_",g):j+g,b=h("",{id:!f.f?c+"_filter":null,"class":b.sFilter}).append(h("").append(j)),f=function(){var b=!this.value? +"":this.value;b!=e.sSearch&&(fa(a,{sSearch:b,bRegex:e.bRegex,bSmart:e.bSmart,bCaseInsensitive:e.bCaseInsensitive}),a._iDisplayStart=0,O(a))},g=null!==a.searchDelay?a.searchDelay:"ssp"===y(a)?400:0,i=h("input",b).val(e.sSearch).attr("placeholder",d.sSearchPlaceholder).bind("keyup.DT search.DT input.DT paste.DT cut.DT",g?ua(f,g):f).bind("keypress.DT",function(a){if(13==a.keyCode)return!1}).attr("aria-controls",c);h(a.nTable).on("search.dt.DT",function(b,c){if(a===c)try{i[0]!==I.activeElement&&i.val(e.sSearch)}catch(d){}}); +return b[0]}function fa(a,b,c){var d=a.oPreviousSearch,e=a.aoPreSearchCols,f=function(a){d.sSearch=a.sSearch;d.bRegex=a.bRegex;d.bSmart=a.bSmart;d.bCaseInsensitive=a.bCaseInsensitive};Ia(a);if("ssp"!=y(a)){wb(a,b.sSearch,c,b.bEscapeRegex!==k?!b.bEscapeRegex:b.bRegex,b.bSmart,b.bCaseInsensitive);f(b);for(b=0;b =b.length)a.aiDisplay=f.slice(); +else{if(g||c||e.length>b.length||0!==b.indexOf(e)||a.bSorted)a.aiDisplay=f.slice();b=a.aiDisplay;for(c=b.length-1;0<=c;c--)d.test(a.aoData[b[c]]._sFilterRow)||b.splice(c,1)}}function Qa(a,b,c,d){a=b?a:va(a);c&&(a="^(?=.*?"+h.map(a.match(/"[^"]+"|[^ ]+/g)||[""],function(a){if('"'===a.charAt(0))var b=a.match(/^"(.*)"$/),a=b?b[1]:a;return a.replace('"',"")}).join(")(?=.*?")+").*$");return RegExp(a,d?"i":"")}function va(a){return a.replace(Zb,"\\$1")}function zb(a){var b=a.aoColumns,c,d,e,f,g,j,i,h,l= +m.ext.type.search;c=!1;d=0;for(f=a.aoData.length;d ",{"class":a.oClasses.sInfo,id:!c?b+"_info":null});c||(a.aoDrawCallback.push({fn:Cb,sName:"information"}),d.attr("role","status").attr("aria-live","polite"),h(a.nTable).attr("aria-describedby",b+"_info"));return d[0]}function Cb(a){var b=a.aanFeatures.i;if(0!==b.length){var c=a.oLanguage, +d=a._iDisplayStart+1,e=a.fnDisplayEnd(),f=a.fnRecordsTotal(),g=a.fnRecordsDisplay(),j=g?c.sInfo:c.sInfoEmpty;g!==f&&(j+=" "+c.sInfoFiltered);j+=c.sInfoPostFix;j=Db(a,j);c=c.fnInfoCallback;null!==c&&(j=c.call(a.oInstance,a,d,e,f,g,j));h(b).html(j)}}function Db(a,b){var c=a.fnFormatNumber,d=a._iDisplayStart+1,e=a._iDisplayLength,f=a.fnRecordsDisplay(),g=-1===e;return b.replace(/_START_/g,c.call(a,d)).replace(/_END_/g,c.call(a,a.fnDisplayEnd())).replace(/_MAX_/g,c.call(a,a.fnRecordsTotal())).replace(/_TOTAL_/g, +c.call(a,f)).replace(/_PAGE_/g,c.call(a,g?1:Math.ceil(d/e))).replace(/_PAGES_/g,c.call(a,g?1:Math.ceil(f/e)))}function ga(a){var b,c,d=a.iInitDisplayStart,e=a.aoColumns,f;c=a.oFeatures;var g=a.bDeferLoading;if(a.bInitialised){nb(a);kb(a);ea(a,a.aoHeader);ea(a,a.aoFooter);C(a,!0);c.bAutoWidth&&Ha(a);b=0;for(c=e.length;b ",{name:c+"_length","aria-controls":c,"class":b.sLengthSelect}), +g=0,j=f.length;g
days
' + - 'hrs
' + - 'mins
'; + text = 'days
' + 'hrs
' + 'mins
'; return text; } else if (seconds >= 3600) { text = 'hrs
' + - 'mins
'; + 'mins
'; return text; } else if (seconds >= 60) { - text = 'mins
'; + text = 'mins
'; return text; } else { text = 'mins
'; @@ -275,13 +274,13 @@ function humanTime(seconds) { function humanTimeClean(seconds) { if (seconds >= 86400) { - text = Math.floor(moment.duration(seconds, 'seconds').asDays()) + ' days ' + - Math.floor(moment.duration((seconds % 86400), 'seconds').asHours()) + ' hrs ' + - Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + ' mins'; + text = Math.floor(moment.duration(seconds, 'seconds').asDays()) + ' days ' + Math.floor(moment.duration(( + seconds % 86400), 'seconds').asHours()) + ' hrs ' + Math.floor(moment.duration( + ((seconds % 86400) % 3600), 'seconds').asMinutes()) + ' mins'; return text; } else if (seconds >= 3600) { - text = Math.floor(moment.duration((seconds % 86400), 'seconds').asHours()) + ' hrs ' + - Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + ' mins'; + text = Math.floor(moment.duration((seconds % 86400), 'seconds').asHours()) + ' hrs ' + Math.floor(moment.duration( + ((seconds % 86400) % 3600), 'seconds').asMinutes()) + ' mins'; return text; } else if (seconds >= 60) { text = Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + ' mins'; @@ -291,37 +290,35 @@ function humanTimeClean(seconds) { return text; } } - String.prototype.toProperCase = function () { - return this.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();}); + return this.replace(/\w\S*/g, function (txt) { + return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); + }); }; function millisecondsToMinutes(ms, roundToMinute) { - - if (ms > 0) { - seconds = ms / 1000; - minutes = seconds / 60; - - if (roundToMinute) { - output = Math.round(minutes, 0) - } else { - minutesFloor = Math.floor(minutes); - secondsReal = Math.round((seconds - (minutesFloor * 60)),0); - if (secondsReal < 10) { - secondsReal = '0' + secondsReal; - } - output = minutesFloor + ':' + secondsReal; - } - return output; - } else { - if (roundToMinute) { - return '0'; - } else { - return '0:00'; - } - } + if (ms > 0) { + seconds = ms / 1000; + minutes = seconds / 60; + if (roundToMinute) { + output = Math.round(minutes, 0) + } else { + minutesFloor = Math.floor(minutes); + secondsReal = Math.round((seconds - (minutesFloor * 60)), 0); + if (secondsReal < 10) { + secondsReal = '0' + secondsReal; + } + output = minutesFloor + ':' + secondsReal; + } + return output; + } else { + if (roundToMinute) { + return '0'; + } else { + return '0:00'; + } + } } - // Our countdown plugin takes a callback, a duration, and an optional message $.fn.countdown = function (callback, duration, message) { // If no message is provided, we use an empty string @@ -334,68 +331,65 @@ $.fn.countdown = function (callback, duration, message) { if (--duration) { // Update our container's message container.html(duration + message); - // Otherwise + // Otherwise } else { // Clear the countdown interval clearInterval(countdown); // And fire the callback passing our container as `this` callback.call(container); } - // Run interval every 1000ms (1 second) + // Run interval every 1000ms (1 second) }, 1000); - }; function setCookie(cname, cvalue, exdays) { var d = new Date(); - d.setTime(d.getTime() + (exdays*24*60*60*1000)); - var expires = "expires="+d.toUTCString(); + d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000)); + var expires = "expires=" + d.toUTCString(); document.cookie = cname + "=" + cvalue + "; " + expires; } function getCookie(cname) { var name = cname + "="; var ca = document.cookie.split(';'); - for(var i=0; iplays
diff --git a/data/interfaces/default/login.html b/data/interfaces/default/login.html new file mode 100644 index 00000000..a535f0e4 --- /dev/null +++ b/data/interfaces/default/login.html @@ -0,0 +1,63 @@ + + + + + +
+ | Timestamp | -Level | -Message | +Timestamp | +Level | +Message |
|---|
| Timestamp | -Level | -Message | +Timestamp | +Level | +Message |
|---|
| Timestamp | -Level | -Message | +Timestamp | +Level | +Message |
|---|
| Timestamp | -Agent | -Action | -Subject Text | -Body Text | -Script Args | +Timestamp | +Agent | +Action | +Subject Text | +Body Text | +Script Args |
|---|
| Timestamp | +User | +User Group | +IP Address | +Host | +Operating System | +Browser | +
|---|
| Log File: | -${os.path.join(config['log_dir'],'plexpy.log')} | +${os.path.join(config['log_dir'], logger.FILENAME)} |
| Backup Directory: | @@ -134,7 +135,7 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
Group successive play history by the same user as a single entry in the tables and watch statistics.
Enable to mask passwords, access tokens, and public IP addresses with asterisks (*) in the logs.
- Note: Only logs from the time this setting is enabled will be masked. Do not post your logs publically without masking sensitive information!
+ Select the sections to show on the homepage. + Drag the items below to reorder your homepage content. +
+
- Select the cards to show in the watch statistics on the home page. Select none to disable.
+ Select the cards to show in the watch statistics on the home page.
Drag the items below to reorder your homepage content.
- Select the cards to show in the library statistics on the home page. Select none to disable.
+ Select the cards to show in the library statistics on the home page.
Drag the items below to reorder your homepage content.
Port to bind web server to. Note that ports below 1024 may require root.
The base URL of the web server. Used for reverse proxies.
+Launch browser pointed to PlexPy, on startup.
+Launch browser pointed to PlexPy on startup.
Password for web server authentication. Leave empty to disable.
+Store a hashed password in the config file.
Warning: Your password cannot be recovered if forgotten!
Allow shared users to login to PlexPy using their Plex.tv account. Individual user access needs to be enabled from Users > Edit Mode.
+Enable if you want PlexPy to calculate the total file size for TV Shows/Seasons and Artists/Albums on the media info tables.
+
+ Enable to mask passwords, access tokens, and public IP addresses with asterisks (*) in the logs.
+ Note: Only logs from the time this setting is enabled will be masked. Do not post your logs publically without masking sensitive information!
+
+ Enable to cache images from Plex to reduce API calls and improve loading times.
+ Note: Video preview thumbnails (BIF) are not cached.
+
Click a button below to import an exisiting database from another app.
+Enable to upload Plex posters to Imgur for notifications. Disable if posters are not being used to save bandwidth.
Enter your Imgur API client ID in order to upload posters.
+ You can register a new application here.
+ Note: The shared Imgur client id will be removed in a future PlexPy update!
+ Please enter your own client id in to continue uploading posters!
+
+
The interval (in seconds) PlexPy will wait for a video item to be active before logging it. 0 to disable.
PlexPy supports a wide variety of notification options. To set up a notification agent configure this in Settings -> Notification Agents after you have completed this setup wizard.
If you have an existing PlexWatch database, you can import the data into PlexPy.
+If you have an existing PlexWatch/Plexivity database, you can import the data into PlexPy.
When you complete this wizard navigate to the settings menu and to the Extra Settings tab. You will find an import tool there - which will convert your plexWatch database into a format that PlexPy can read. + which will convert your PlexWatch/Plexivity database into a format that PlexPy can read.
%r" - ... % (a, e)) - ... - >>> class Color(UniqueEnum): - ... red = 1 - ... green = 2 - ... blue = 3 - ... grene = 2 - Traceback (most recent call last): - ... - ValueError: aliases not allowed in UniqueEnum: 'grene' --> 'green' - - -OrderedEnum -^^^^^^^^^^^ - -An ordered enumeration that is not based on ``IntEnum`` and so maintains -the normal ``Enum`` invariants (such as not being comparable to other -enumerations):: - - >>> class OrderedEnum(Enum): - ... def __ge__(self, other): - ... if self.__class__ is other.__class__: - ... return self._value_ >= other._value_ - ... return NotImplemented - ... def __gt__(self, other): - ... if self.__class__ is other.__class__: - ... return self._value_ > other._value_ - ... return NotImplemented - ... def __le__(self, other): - ... if self.__class__ is other.__class__: - ... return self._value_ <= other._value_ - ... return NotImplemented - ... def __lt__(self, other): - ... if self.__class__ is other.__class__: - ... return self._value_ < other._value_ - ... return NotImplemented - ... - >>> class Grade(OrderedEnum): - ... __ordered__ = 'A B C D F' - ... A = 5 - ... B = 4 - ... C = 3 - ... D = 2 - ... F = 1 - ... - >>> Grade.C < Grade.A - True - - -Planet -^^^^^^ - -If ``__new__`` or ``__init__`` is defined the value of the enum member -will be passed to those methods:: - - >>> class Planet(Enum): - ... MERCURY = (3.303e+23, 2.4397e6) - ... VENUS = (4.869e+24, 6.0518e6) - ... EARTH = (5.976e+24, 6.37814e6) - ... MARS = (6.421e+23, 3.3972e6) - ... JUPITER = (1.9e+27, 7.1492e7) - ... SATURN = (5.688e+26, 6.0268e7) - ... URANUS = (8.686e+25, 2.5559e7) - ... NEPTUNE = (1.024e+26, 2.4746e7) - ... def __init__(self, mass, radius): - ... self.mass = mass # in kilograms - ... self.radius = radius # in meters - ... @property - ... def surface_gravity(self): - ... # universal gravitational constant (m3 kg-1 s-2) - ... G = 6.67300E-11 - ... return G * self.mass / (self.radius * self.radius) - ... - >>> Planet.EARTH.value - (5.976e+24, 6378140.0) - >>> Planet.EARTH.surface_gravity - 9.802652743337129 - - -How are Enums different? ------------------------- - -Enums have a custom metaclass that affects many aspects of both derived Enum -classes and their instances (members). - - -Enum Classes -^^^^^^^^^^^^ - -The ``EnumMeta`` metaclass is responsible for providing the -``__contains__``, ``__dir__``, ``__iter__`` and other methods that -allow one to do things with an ``Enum`` class that fail on a typical -class, such as ``list(Color)`` or ``some_var in Color``. ``EnumMeta`` is -responsible for ensuring that various other methods on the final ``Enum`` -class are correct (such as ``__new__``, ``__getnewargs__``, -``__str__`` and ``__repr__``) - - -Enum Members (aka instances) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The most interesting thing about Enum members is that they are singletons. -``EnumMeta`` creates them all while it is creating the ``Enum`` -class itself, and then puts a custom ``__new__`` in place to ensure -that no new ones are ever instantiated by returning only the existing -member instances. - - -Finer Points -^^^^^^^^^^^^ - -Enum members are instances of an Enum class, and even though they are -accessible as ``EnumClass.member``, they are not accessible directly from -the member:: - - >>> Color.red -