commit 28f7c4f102b16122332704d38c2652dc8852a872 Author: j Date: Tue May 8 12:31:41 2018 +0100 cinemuse diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config.jsonc b/config.jsonc new file mode 100644 index 0000000..3ad9cd2 --- /dev/null +++ b/config.jsonc @@ -0,0 +1,1422 @@ +/* +------------------------------------------------------------------------------- +pan.do/ra Configuration +------------------------------------------------------------------------------- +You can edit this file. Most changes are effective immediately; everything that +affects the database layout (adding a new item key, annotation layer etc.) will +require a server restart. You may also want to take a look at the configuration +examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution. +*/ +{ + /* + "annotations" configures the annotation panel. + "showUsers": If true, include list of users in menu, so that + annotations by specific users can be turned on and off + */ + "annotations": { + "showUsers": true + }, + /* + "cantPlay" specifies the UI for clips or videos that a user is not allowed + to play. + "icon": Symbol name (see https://oxjs.org/#examples/symbols/live) + "link": URL loaded on click (for example "/rights", if "rights" is + defined in "sitePages") + "text": Text shown on mouseover + */ + "cantPlay": { + "icon": "noCopyright", + "link": "", + "text": "" + }, + /* + "capabilities" are permissions per user level. + Depending on the capability, this is either global: + {level: true} means a user of that level has the capability + or related to the rights levels of items or texts: + {level: x} means a user of that level has the capability for items or + texts of a rights level up to and including x + */ + "capabilities": { + "canAddItems": {"member": true, "staff": true, "admin": true}, + "canAddDocuments": {"member": true, "staff": true, "admin": true}, + "canDownloadVideo": {"guest": 1, "member": 1, "staff": 4, "admin": 4}, + "canEditAnnotations": {"staff": true, "admin": true}, + "canEditDocuments": {"staff": true, "admin": true}, + "canEditEntities": {"staff": true, "admin": true}, + "canEditEvents": {"staff": true, "admin": true}, + "canEditFeaturedCollections": {"staff": true, "admin": true}, + "canEditFeaturedEdits": {"staff": true, "admin": true}, + "canEditFeaturedLists": {"staff": true, "admin": true}, + "canEditFeaturedTexts": {"staff": true, "admin": true}, + "canEditMedia": {"staff": true, "admin": true}, + "canEditMetadata": {"staff": true, "admin": true}, + "canEditPlaces": {"staff": true, "admin": true}, + "canEditRightsLevel": {"member": true, "staff": true, "admin": true}, + "canEditSitePages": {"staff": true, "admin": true}, + "canEditUsers": {"admin": true}, + "canExportAnnotations": {"member": true, "staff": true, "admin": true}, + "canImportAnnotations": {"member": true, "staff": true, "admin": true}, + "canImportItems": {"member": true, "staff": true, "admin": true}, + "canManageDocuments": {"member": true, "staff": true, "admin": true}, + "canManageEntities": {"member": true, "staff": true, "admin": true}, + "canManageHome": {"staff": true, "admin": true}, + "canManagePlacesAndEvents": {"member": true, "staff": true, "admin": true}, + "canManageTitlesAndNames": {"member": true, "staff": true, "admin": true}, + "canManageUsers": {"staff": true, "admin": true}, + "canPlayClips": {"guest": 1, "member": 1, "staff": 4, "admin": 4}, + "canPlayVideo": {"guest": 1, "member": 1, "staff": 4, "admin": 4}, + "canReadText": {"guest": 0, "member": 0, "staff": 1, "admin": 1}, + "canRemoveItems": {"admin": true}, + "canRemoveDocuments": {"admin": true}, + "canSeeAccessed": {"staff": true, "admin": true}, + "canSeeAllTasks": {"staff": true, "admin": true}, + "canSeeDebugMenu": {"staff": true, "admin": true}, + "canSeeExtraItemViews": {"staff": true, "admin": true}, + "canSeeMedia": {"staff": true, "admin": true}, + "canSeeDocument": {"guest": 1, "member": 1, "staff": 4, "admin": 4}, + "canSeeItem": {"guest": 1, "member": 1, "staff": 4, "admin": 4}, + "canSeeSize": {"staff": true, "admin": true}, + "canSeeSoftwareVersion": {"staff": true, "admin": true}, + "canSendMail": {"admin": true} + }, + /* + "clipKeys" are the properties that clips can be sorted by (the values are + populated automatically). If "sortOperator" is not specified, it will be + "+" (ascending) for strings and "-" (descending) for numbers. + */ + "clipKeys": [ + {"id": "text", "title": "Text", "type": "string"}, + {"id": "position", "title": "Position", "type": "float", "sortOperator": "+"}, + {"id": "duration", "title": "Duration", "type": "float"}, + {"id": "hue", "title": "Hue", "type": "float", "sortOperator": "+"}, + {"id": "saturation", "title": "Saturation", "type": "float"}, + {"id": "lightness", "title": "Lightness", "type": "float"}, + {"id": "volume", "title": "Volume", "type": "float"} + ], + /* + "clipLayers" is the ordered list of public layers that will appear as the + text of clips (in grid view, below the icon). Excluding a layer from this + list means it will not be included in find annotations. + */ + "clipLayers": ["notes", "keywords", "subtitles"], + /* + "documentKeys" defines the metadata associated with each document. Required keys + are "*", "id" and "title". + A documentKey must have the following properties: + "id": The unique id of the key (as used by the server) + "title": The title of the key (as displayed by the client) + "type": Can be "boolean", "date", "enum", "float", "hue", "integer", + "layer", "string", "text", "time" or ["..."] (list of values of + this type). If type is "layer", this is a reference to the + annotations layer with the same id. + and can have any of the following properties: + "additionalSort": Ordered list of {key, operator} objects, where key is + another itemKey and operator is "+" or "-". This can be used to + override user.ui.listSort when results are sorted by this key. + "autocomplete": If true, the find element will provide autocomplete + "autocompleteSort": Sort order of autocomplete suggestions + "capability": A capability required to see data for this key + "columnRequired": If true, the column can't be removed from list view + "columnWidth": Default column width in px. If absent, no column for + this key can be added in list view. + "filter": If true, one can filter results by this key + "find": If true, this key will appear as an option in the find element + "flag": Can be "country" or "language". If set (and filter is true), a + flag icon corresponding to the field's value will be displayed. + "format": {type: string, args: [value, value, ...]}, used for special + formatting. This will invoke Ox.formatType(args). For details, see + https://oxjs.org/#doc/Ox.formatArea etc. + "secondaryId": If true, loading the URL "/value" will redirect to the + corresponding item, in case there is an exact match for this key + "sort": If true, one can sort results by this key + "sortOperator": Sort order ("+" or "-"), in case it differs from the + default for the key's type ("+" for strings, "-" for numbers) + "sortType": Special sort type ("person" or "title") which can be + further configured in "Manage Names" or "Manage Titles" + "value": {key: string, type: string} or {layer: string, type: string}, + for keys whose value is derived from other keys or layers (like + "number of actors" or "words per minute"). Possible values for type + are "length", "lengthperminute", "words", and "wordsperminute". + Alternatively, "value" can be set to the string "capability", which + results in an itemKey whose boolean value indicates the presence or + absence of a userLevel-dependent capability. This can be used to + create queries and lists like "all items this user can play" etc. + "values": [value, value, ...] Ordered list of values, in case "type" is + "enum" + */ + "documentKeys": [ + { + "id": "*", + "title": "All", + "type": "text", + "find": true + }, + { + "id": "title", + "operator": "+", + "title": "Title", + "type": "string", + "find": true, + "sort": true, + "sortType": "title", + "autocomplete": true, + "columnWidth": 256 + }, + { + "id": "type", + "operator": "+", + "title": "Type", + "type": "string", + "filter": true, + "find": true, + "sort": true, + "autocomplete": true, + "columnWidth": 128 + }, + { + "id": "author", + "operator": "+", + "title": "Author", + "type": ["string"], + "filter": true, + "find": true, + "sort": true, + "sortType": "person", + "autocomplete": true, + "columnWidth": 256 + }, + { + "id": "publisher", + "operator": "+", + "title": "Publisher", + "type": "string", + "filter": true, + "find": true, + "sort": true, + "autocomplete": true, + "columnWidth": 256 + }, + { + "id": "place", + "title": "Place", + "type": ["string"], + "columnWidth": 128, + "filter": true, + "find": true, + "sort": true + }, + { + "id": "date", + "title": "Date", + "type": "string", + "columnWidth": 120, + //"format": {"type": "date", "args": ["%a, %b %e, %Y"]}, + "sort": true + }, + { + "id": "series", + "title": "Series", + "type": "string", + "columnWidth": 128, + "find": true, + "sort": true + }, + { + "id": "edition", + "title": "Edition", + "type": "string", + "columnWidth": 128, + "find": true + }, + { + "id": "language", + "title": "Language", + "type": ["string"], + "columnWidth": 128, + "filter": true, + "find": true, + "sort": true + }, + { + "id": "id", + "operator": "+", + "title": "ID", + "type": "string", + "sort": true, + "columnWidth": 64 + }, + { + "id": "extension", + "operator": "+", + "title": "Extension", + "type": "string", + "filter": true, + "find": true, + "sort": true, + "autocomplete": true, + "columnWidth": 64 + }, + { + "id": "dimensions", + "operator": "-", + "title": "Dimensions", + "type": "integer", + "sort": true, + "columnWidth": 128 + }, + { + "id": "size", + "operator": "-", + "title": "Size", + "type": "integer", + "sort": true, + "format": {"type": "value", "args": ["B"]}, + "columnWidth": 64 + }, + { + "id": "description", + "operator": "+", + "title": "Description", + "type": "text", + "find": true, + "sort": true, + "columnWidth": 256 + }, + { + "id": "matches", + "operator": "-", + "title": "Matches", + "type": "integer", + "sort": true, + "columnWidth": 64 + }, + { + "id": "user", + "operator": "+", + "title": "User", + "type": "string", + "filter": true, + "find": true, + "sort": true, + "autocomplete": true, + "columnWidth": 128 + }, + { + "id": "created", + "operator": "-", + "title": "Created", + "format": {"type": "date", "args": ["%Y-%m-%d %H:%M:%S"]}, + "type": "date", + "sort": true, + "columnWidth": 144 + }, + { + "id": "modified", + "operator": "-", + "title": "Modified", + "format": {"type": "date", "args": ["%Y-%m-%d %H:%M:%S"]}, + "type": "date", + "sort": true, + "columnWidth": 144 + }, + { + "id": "accessed", + "title": "Last Accessed", + "type": "date", + "capability": "canSeeAccessed", + "columnWidth": 150, + "format": {"type": "date", "args": ["%Y-%m-%d %H:%M:%S"]}, + "sort": true + }, + { + "id": "timesaccessed", + "title": "Times Accessed", + "type": "integer", + "capability": "canSeeAccessed", + "columnWidth": 60, + "format": {"type": "number", "args": []}, + "sort": true + }, + { + "id": "rightslevel", + "title": "Rights Level", + "type": "enum", + "columnWidth": 90, + "format": {"type": "ColorLevel", "args": [ + ["Public", "Out of Copyright", "Under Copyright", "Private"] + ]}, + "sort": true, + "sortOperator": "+", + "values": ["Public", "Out of Copyright", "Under Copyright", "Private", "Unknown"] + } + ], + /* + "documentRightsLevel" defines which initial rights level will be assigned to documents + created by users of these user levels. + */ + "documentRightsLevel": {"member": 0, "staff": 0, "admin": 0}, + /* + "documentRightsLevels" is an ordered list of rights levels, one of which will be + assigned to each document. + */ + "documentRightsLevels": [ + {"name": "Public", "color": [128, 255, 128]}, + {"name": "Private", "color": [255, 128, 128]} + ], + /* + "entities" can be used to store arbitrary data. They can be referenced in + annotations, info view, or elsewhere. Each entry defines a specific class + of entity object, its properties and their types (for example an "actor" + with "name", "biography", "portrait" etc). The HTML representation for + entities can be customized by adding "/static/js/entity.SITENAME.js". + Each entity has the following properties: + "id": Unique internal ID (can be referenced in "layers") + "title": Human-readable title + "keys": List of properties + "sortType": Optional. If set to "person" or "title", then the sort + order can be configured in "Manage Names" or "Manage Titles" + Each key has the properties "id", "title" and "type". "type" can be + "boolean", "number", "string", "text", "document" (reference to the id of a + document added via "Manage Documents") or ["string"] (list of strings). + "id" and "name" keys are required. If "alternativeNames" is present, these + will act as synonyms when autocompleting values entered as annotations. + */ + "entities": [ + { + "id": "people", + "title": "People", + "keys": [ + {"id": "id", "title": "ID", "type": "string"}, + {"id": "name", "title": "Name", "type": "string"}, + {"id": "additionalNames", "title": "Additional Names", "type": ["string"]}, + {"id": "biography", "title": "Biography", "type": "text"}, + {"id": "portrait", "title": "Portrait", "type": "document"} + ], + "sortType": "person" + } + ], + /* + "flags", if set to true, will cause flag icons to appear in filters. + */ + "flags": true, + /* + "help" specifies the sections of the help dialog. + */ + "help": [ + {"id": "help", "title": "Help"}, + {"id": "accounts", "title": "Accounts"}, + {"id": "navigation", "title": "Navigation"}, + {"id": "views", "title": "Views"}, + {"id": "timelines", "title": "Timelines"}, + {"id": "clips", "title": "Clips"}, + {"id": "maps", "title": "Maps"}, + {"id": "calendars", "title": "Calendars"}, + {"id": "find", "title": "Find"}, + {"id": "filters", "title": "Filters"}, + {"id": "lists", "title": "Lists"}, + {"id": "player", "title": "Player"}, + {"id": "editor", "title": "Editor"}, + {"id": "documents", "title": "Documents"}, + {"id": "entities", "title": "Entities"}, + {"id": "edits", "title": "Edits"}, + {"id": "texts", "title": "Texts"}, + {"id": "embeds", "title": "Embeds"} + ], + /* + "importMetadata" defines what fields (as defined in "itemKeys") will get + populated with imported metadata. There is metadata for "title", + "description", "uploader", "date", "tags", "id", "url", and the value must + be a format string (like "{title} ({id})"). The type of the itemKeys must + be "string", ["string"], "text", "date" or "year". The last two only work + with "{date}". + */ + "importMetadata": { + "title": "{title}" + }, + /* + "itemKeys" defines the metadata associated with each item. Required keys + are "*", "id" and "title". Annotation layers can be referenced too, which + makes them available in the find element. Adding a key with "annotations" + as id adds a "find in any annotation layer" option. Further, adding a key + with "random" as id can be used for a random sort order. + An itemKey must have the following properties: + "id": The unique id of the key (as used by the server) + "title": The title of the key (as displayed by the client) + "type": Can be "boolean", "date", "enum", "float", "hue", "integer", + "layer", "string", "text", "time" or ["..."] (list of values of + this type). If type is "layer", this is a reference to the + annotations layer with the same id. + and can have any of the following properties: + "additionalSort": Ordered list of {key, operator} objects, where key is + another itemKey and operator is "+" or "-". This can be used to + override user.ui.listSort when results are sorted by this key. + "autocomplete": If true, the find element will provide autocomplete + "autocompleteSort": Sort order of autocomplete suggestions + "capability": A capability required to see data for this key + "columnRequired": If true, the column can't be removed from list view + "columnWidth": Default column width in px. If absent, no column for + this key can be added in list view. + "filter": If true, one can filter results by this key + "find": If true, this key will appear as an option in the find element + "flag": Can be "country" or "language". If set (and filter is true), a + flag icon corresponding to the field's value will be displayed. + "format": {type: string, args: [value, value, ...]}, used for special + formatting. This will invoke Ox.formatType(args). For details, see + https://oxjs.org/#doc/Ox.formatArea etc. + "secondaryId": If true, loading the URL "/value" will redirect to the + corresponding item, in case there is an exact match for this key + "sort": If true, one can sort results by this key + "sortOperator": Sort order ("+" or "-"), in case it differs from the + default for the key's type ("+" for strings, "-" for numbers) + "sortType": Special sort type ("person" or "title") which can be + further configured in "Manage Names" or "Manage Titles" + "value": {key: string, type: string} or {layer: string, type: string}, + for keys whose value is derived from other keys or layers (like + "number of actors" or "words per minute"). Possible values for type + are "length", "lengthperminute", "words", and "wordsperminute". + Alternatively, "value" can be set to the string "capability", which + results in an itemKey whose boolean value indicates the presence or + absence of a userLevel-dependent capability. This can be used to + create queries and lists like "all items this user can play" etc. + "values": [value, value, ...] Ordered list of values, in case "type" is + "enum" + */ + "itemKeys": [ + { + "id": "*", + "title": "All", + "type": "text", + "find": true + }, + { + "id": "title", + "title": "Title", + "type": "string", + "autocomplete": true, + "autocompleteSort": [{"key": "timesaccessed", "operator": "-"}], + "columnRequired": true, + "columnWidth": 180, + "find": true, + "sort": true, + "sortType": "title" + }, + { + "id": "name", + "title": "Name", + "type": ["string"], + "autocomplete": true, + "find": true + }, + { + "id": "director", + "title": "Director", + "type": ["string"], + "autocomplete": true, + "columnRequired": true, + "columnWidth": 180, + "filter": true, + "sort": true, + "sortType": "person" + }, + { + "id": "country", + "title": "Country", + "type": "string", + "autocomplete": true, + "columnWidth": 180, + "filter": true, + "find": true, + "flag": "country", + "sort": true + }, + { + "id": "year", + "title": "Year", + "type": "year", + "additionalSort": [ + {"key": "director", "operator": "+"}, + {"key": "title", "operator": "+"} + ], + "autocomplete": true, + "columnWidth": 60, + "filter": true, + "find": true, + "sort": true + }, + + { + "id": "type", + "title": "Type", + "type": ["string"], + "columnWidth": 128, + "filter": true, + "find": true, + "sort": true + }, + { + "id": "framework", + "title": "Framework", + "type": ["string"], + "columnWidth": 128, + "filter": true, + "find": true, + "sort": true + }, + { + "id": "style", + "title": "Style", + "type": ["string"], + "columnWidth": 128, + "filter": true, + "find": true, + "sort": true + }, + + { + "id": "language", + "title": "Language", + "type": ["string"], + "autocomplete": true, + "columnWidth": 120, + "filter": true, + "find": true, + "sort": true + }, + { + "id": "runtime", + "title": "Runtime", + "type": "time", + "columnWidth": 60, + "format": {"type": "duration", "args": [0, "short"]}, + "sort": true + }, + { + "id": "color", + "title": "Color", + "type": ["string"], + "columnWidth": 120, + "filter": true, + "sort": true + }, + { + "id": "sound", + "title": "Sound", + "type": ["string"], + "columnWidth": 120, + "filter": true, + "sort": true + }, + { + "id": "writer", + "title": "Writer", + "type": ["string"], + "autocomplete": true, + "columnWidth": 180, + "filter": true, + "find": true, + "sort": true, + "sortType": "person" + }, + { + "id": "producer", + "title": "Producer", + "type": ["string"], + "autocomplete": true, + "columnWidth": 180, + "filter": true, + "find": true, + "sort": true, + "sortType": "person" + }, + { + "id": "cinematographer", + "title": "Cinematographer", + "type": ["string"], + "autocomplete": true, + "columnWidth": 180, + "filter": true, + "find": true, + "sort": true, + "sortType": "person" + }, + { + "id": "editor", + "title": "Editor", + "type": ["string"], + "autocomplete": true, + "columnWidth": 180, + "filter": true, + "find": true, + "sort": true, + "sortType": "person" + }, + { + "id": "actor", + "title": "Actor", + "type": ["string"], + "autocomplete": true, + "filter": true, + "find": true, + "sortType": "person" + }, + { + "id": "numberofactors", + "title": "Number of Actors", + "type": "integer", + "columnWidth": 60, + "sort": true, + "value": {"key": "actor", "type": "length"} + }, + { + "id": "character", + "title": "Character", + "type": ["string"], + "autocomplete": true, + "find": true, + "sortType": "string" + }, + { + "id": "name", + "title": "Name", + "type": ["string"], + "autocomplete": true, + "find": true + }, + { + "id": "productionCompany", + "title": "Production Company", + "type": ["string"], + "autocomplete": true, + "columnWidth": 180, + "filter": true, + "find": true, + "sort": true + }, + { + "id": "genre", + "title": "Genre", + "type": ["string"], + "autocomplete": true, + "columnWidth": 120, + "filter": true, + "find": true, + "sort": true + }, + { + "id": "topic", + "title": "Topic", + "type": ["string"], + "autocomplete": true, + "filter": true, + "find": true + }, + + { + "id": "summary", + "title": "Summary", + "type": "text", + "find": true + }, + { + "id": "notes", + "title": "Notes", + "type": "text", + "capability": "canEditMetadata" + }, + { + "id": "id", + "title": "ID", + "type": "string", + "columnWidth": 90, + "sort": true + }, + { + "id": "imdbId", + "title": "IMDb ID", + "type": "string", + "columnWidth": 90, + "secondaryId": true, + "sort": true + }, + { + "id": "comments", + "title": "Comments", + "type": "text", + "capability": "canEditMetadata" + }, + { + "id": "keywords", + "title": "Keywords", + "type": "layer", + "filter": true, + "find": true + }, + + { + "id": "subtitles", + "title": "Subtitles", + "type": "layer", + "find": true + }, + { + "id": "notes", + "title": "Notes", + "type": "layer", + "find": true + }, + { + "id": "numberofannotations", + "title": "Annotations", + "type": "integer", + "columnWidth": 60, + "sort": true + }, + { + "id": "duration", + "title": "Duration", + "type": "time", + "columnWidth": 90, + "format": {"type": "duration", "args": [0, "short"]}, + "sort": true + }, + { + "id": "resolution", + "title": "Resolution", + "type": ["integer"], + "columnWidth": 90, + "format": {"type": "resolution", "args": ["px"]}, + "sort": true + }, + { + "id": "aspectratio", + "title": "Aspect Ratio", + "type": "float", + "columnWidth": 90, + "format": {"type": "unit", "args": [":1", 3]}, + "sort": true + }, + { + "id": "pixels", + "title": "Pixels", + "type": "integer", + "columnWidth": 90, + "format": {"type": "value", "args": ["px"]}, + "sort": true + }, + { + "id": "hue", + "title": "Hue", + "type": "hue", + "columnWidth": 90, + "format": {"type": "color", "args": ["hue"]}, + "sort": true, + "sortOperator": "+" + }, + { + "id": "saturation", + "title": "Saturation", + "type": "float", + "columnWidth": 90, + "format": {"type": "color", "args": ["saturation"]}, + "sort": true + }, + { + "id": "lightness", + "title": "Lightness", + "type": "float", + "columnWidth": 90, + "format": {"type": "color", "args": ["lightness"]}, + "sort": true + }, + { + "id": "volume", + "title": "Volume", + "type": "float", + "columnWidth": 60, + "format": {"type": "color", "args": ["lightness"]}, + "sort": true + }, + { + "id": "numberofcuts", + "title": "Number of Cuts", + "type": "integer", + "columnWidth": 60, + "format": {"type": "number", "args": []}, + "sort": true, + "value": {"key": "cuts", "type": "length"} + }, + { + "id": "cutsperminute", + "title": "Cuts per Minute", + "type": "float", + "columnWidth": 60, + "format": {"type": "number", "args": [3]}, + "sort": true, + "value": {"key": "cuts", "type": "lengthperminute"} + }, + { + "id": "words", + "title": "Number of Words", + "type": "integer", + "columnWidth": 60, + "format": {"type": "number", "args": []}, + "sort": true, + "value": {"layer": "subtitles", "type": "words"} + }, + { + "id": "wordsperminute", + "title": "Words per Minute", + "type": "float", + "columnWidth": 60, + "format": {"type": "number", "args": [3]}, + "sort": true, + "value": {"layer": "subtitles", "type": "wordsperminute"} + }, + { + "id": "size", + "title": "Size", + "type": "integer", + "capability": "canSeeMedia", + "columnWidth": 60, + "format": {"type": "value", "args": ["B"]}, + "sort": true + }, + { + "id": "bitrate", + "title": "Bitrate", + "type": "integer", + "columnWidth": 60, + "format": {"type": "value", "args": ["bps"]}, + "sort": true + }, + { + "id": "numberoffiles", + "title": "Number of Files", + "type": "integer", + "capability": "canSeeMedia", + "columnWidth": 60, + "sort": true, + "value": {"key": "files", "type": "length"} + }, + { + "id": "user", + "title": "User", + "type": "string", + "capability": "canSeeMedia", + "find": true + }, + { + "id": "created", + "title": "Date Created", + "type": "date", + "columnWidth": 120, + "format": {"type": "date", "args": ["%Y-%m-%d %H:%M:%S"]}, + "sort": true + }, + { + "id": "modified", + "title": "Last Modified", + "type": "date", + "columnWidth": 90, + "format": {"type": "date", "args": ["%Y-%m-%d %H:%M:%S"]}, + "sort": true + }, + { + "id": "accessed", + "title": "Last Accessed", + "type": "date", + "capability": "canSeeAccessed", + "columnWidth": 90, + "format": {"type": "date", "args": ["%Y-%m-%d %H:%M:%S"]}, + "sort": true + }, + { + "id": "timesaccessed", + "title": "Times Accessed", + "type": "integer", + "capability": "canSeeAccessed", + "columnWidth": 60, + "format": {"type": "number", "args": []}, + "sort": true + }, + { + "id": "rightslevel", + "title": "Rights Level", + "type": "enum", + "columnWidth": 90, + "format": {"type": "ColorLevel", "args": [ + ["Public", "Private"] + ]}, + "sort": true, + "sortOperator": "+", + "values": ["Public", "Private", "Unknown"] + }, + { + "id": "random", + "title": "Random", + "type": "integer", + "sort": true + } + ], + /* + "itemName" specifies the name of items ("Movies", "Videos" or similar). + Note that anything excessively long may cause layout errors. + */ + "itemName": { + "singular": "Film", + "plural": "Films" + }, + /* + "itemRequiresVideo" regulates if items without video can be created. If set + to true, the only way to add a new item is to upload a new video. + */ + "itemRequiresVideo": true, + /* + "itemTitleKeys" is a list of itemKeys required to compose the item title + displayed at the top of the screen. This title can be customized by adding + "/static/js/getItemTitle.SITENAME.js". + */ + "itemTitleKeys": ["title", "director", "year"], + /* + "itemViews" is an ordered list of available item views. Implemented views + are "info", "documents", "player", "editor", "timeline", "clips", "map", + "calendar", "data" and "media". + */ + "itemViews": [ + {"id": "info", "title": "Info"}, + {"id": "player", "title": "Player"}, + {"id": "editor", "title": "Editor"}, + {"id": "timeline", "title": "Timeline"}, + {"id": "clips", "title": "Clips"}, + {"id": "map", "title": "Map"}, + {"id": "calendar", "title": "Calendar"}, + {"id": "documents", "title": "Documents"}, + {"id": "data", "title": "Data"}, + {"id": "media", "title": "Media"} + ], + /* + "language" is the default language for annotations of type "text". Text in + other languages can be added via markup, for example: + VoilĂ ! + */ + "language": "en", + /* + "languages" is the list of languages that the interface can be switched to. + Currently, localization exists for "ar", "el", "en" and "hi". + */ + "languages": ["ar", "el", "en", "hi"], + /* + "layers" defines the types of time-based annotations that can be entered. + Required keys are: + "id": Internal ID + "item": Name of one such annotation (singular) + "title": Display title + "type": Can be "entity", "string" or "text" (strings cannot contain + line breaks, text can contain HTML markup). + Optional keys are: + "autocomplete": Available if the layer is used as a filter + "canAddAnnotations": Permissions per user level + "canPlayClips": If true, clips from this layer will play for users + with canPlayClips access + "entity": ID of the referenced entity (if type is "entity") + "hasEvents": If true, the calendar will be populated with matches from + this layer + "hasPlaces": If true, the map will be populated with matches from this + layer + "isSubtitles": If true, this layer will be displayed as subtitles + "overlap": If true, overlapping annotations are allowed. Note that + enforcement of "overlap": false is not implemented + "showInfo": If true, user and creation time will be displayed in the + tooltip that appears on mouseover. + */ + "layers": [ + { + "id": "keywords", + "title": "Keywords", + "canAddAnnotations": {"member": true, "staff": true, "admin": true}, + "item": "Keyword", + "autocomplete": true, + "overlap": true, + "type": "string" + }, + { + "id": "notes", + "title": "Notes", + "canAddAnnotations": {"member": true, "staff": true, "admin": true}, + "item": "Note", + "overlap": true, + "showInfo": true, + "type": "text" + }, + { + "id": "internalnotes", + "title": "Internal Notes", + "canAddAnnotations": {"member": true, "staff": true, "admin": true}, + "item": "Internal Note", + "overlap": true, + "showInfo": true, + "type": "text" + }, + { + "id": "subtitles", + "title": "Subtitles", + "canAddAnnotations": {"staff": true, "admin": true}, + "canPlayClips": true, + "hasEvents": true, + "hasPlaces": true, + "isSubtitles": true, + "item": "Subtitle", + "type": "text" + } + ], + /* + "license" can be used to implement instance-specific license settings + */ + "license": null, + /* + "listViews" is an ordered list of available list views. Implemented views + are "list", "grid", "timelines", "clips", "clip", "map" and "calendar". + */ + "listViews": [ + {"id": "list", "title": "as List"}, + {"id": "grid", "title": "as Grid"}, + {"id": "timelines", "title": "with Timelines"}, + {"id": "clips", "title": "with Clips"}, + {"id": "clip", "title": "as Clips"}, + {"id": "map", "title": "on Map"}, + {"id": "calendar", "title": "on Calendar"} + ], + /* + "media" defines additional media that gets added when importing items. + "importPosters": If true, a poster file will be imported (if present) + "importFrames": If true, 3 full-resolution frames per file will be + imported. This is useful to render high-resolution icons and + posters for an instance that has only low-resolution video. + */ + "media": { + "importPosters": false, + "importFrames": false + }, + /* + "menuExtras" can be used to add extra functionality to the main menu bar. + The plug-in architecture is not yet finalized, documentation forthcoming. + */ + "menuExtras": [ + "upload", + "user", + // "locale", + "reload" + ], + /* + "personalLists" specifies which pre-defined lists a new member will have + after signup. "title" is required. If "query" is present, this defines a + smart list. "query" can be any pan.do/ra query object, see /api/find for + further documentation. If you need to reference the username (for example + in order to include a default "My Videos" list), you can use the syntax + {"user": "{username}"} + */ + "personalLists": [ + {"title": "Favorites"} + ], + /* + "posters" contains details about the poster icons. "ratio" is the default + ratio (used to display a placeholder while poster icons are loading). + */ + "posters": { + "ratio": 0.625 + }, + /* + "rightsLevel" defines which initial rights level will be assigned to items + and texts created by users of these user levels. + */ + "rightsLevel": {"member": 2, "staff": 2, "admin": 2}, + /* + "rightsLevels" is an ordered list of rights levels, one of which will be + assigned to each item. + */ + "rightsLevels": [ + {"name": "Public", "color": [128, 255, 128]}, + {"name": "Private", "color": [255, 128, 128]} + ], + /* + "sendReferrer", if set to false, will cause all outgoing links to originate + from one single URL + */ + "sendReferrer": false, + /* + "site" contains various settings for this instance. In "email", "contact" + if the address in the contact form (to), "system" is the address used by + the system (from). + */ + "site": { + "description": "This is a demo of pan.do/ra - a free, open source media archive. It allows you to manage large, decentralized collections of video, to collaboratively create metadata and time-based annotations, and to serve your archive as a cutting-edge web application.", + "email": { + // E-mail address in contact form (to) + "contact": "system@pandora.local", + "footer": "-- \npan.do/ra - http://pan.do/ra", + "prefix": "pan.do/ra News -", + // E-mail address uses by the system (from) + "system": "system@pandora.local" + }, + "https": false, + "id": "cinemuse", + "name": "CineMuseSpace", + // Set to true to allow search engines to index the site + "public": false, + "sendReferrer": true, + "url": "pandora.local" + }, + /* + "sitePages" defines the sections of the main site dialog. If "news" is + present, this will add an interface to add news items. If "contact" is + present, this will add an interface to contact the site owners. + */ + "sitePages": [ + {"id": "about", "title": "About"}, + //{"id": "news", "title": "News"}, + //{"id": "tour", "title": "Take a Tour"}, + //{"id": "faq", "title": "Frequently Asked Questions"}, + //{"id": "terms", "title": "Terms of Service"}, + //{"id": "license", "title": "License"}, + {"id": "contact", "title": "Contact"} + ], + /* + "sites" specifies which other pan.do/ra instances, if any, will appear in + the user interface for creating embeds. This allows for easier creation of + cross-instance references. + */ + "sites": [ + {"name": "0xDB", "url": "0xdb.org", "https": true}, + {"name": "Pad.ma", "url": "pad.ma", "https": true}, + {"name": "Indiancine.ma", "url": "indiancine.ma", "https": true} + ], + /* + "textRightsLevels" defines a list of rights levels for texts. + */ + "textRightsLevels": [ + {"name": "Public", "color": [128, 255, 128]}, + {"name": "Private", "color": [255, 128, 128]} + ], + /* + "themes" is a list of themes that the user interface can be switched to. + Currently available themes are "oxlight", "oxmedium" and "oxdark". The + default theme can be set in user.ui.theme. + */ + "themes": ["oxlight", "oxmedium", "oxdark"], + /* + "timelines" is a list of timeline types. Implemented types are "antialias", + "slitscan", "keyframes" and "audio". + */ + "timelines": [ + {"id": "antialias", "title": "Anti-Alias"}, + {"id": "slitscan", "title": "Slit-Scan"}, + {"id": "keyframes", "title": "Keyframes"}, + {"id": "audio", "title": "Waveform"} + ], + /* + "totals" specifies which totals are displayed in the status bar at the + bottom of list views. Possible ids are "duration", "files", "items", + "pixels" and "size". Adding a capability limits the display of a specific + total to users of the corresponding user levels. + */ + "totals": [ + {"id": "items"}, + {"id": "files", "capability": "canSeeMedia"}, + {"id": "duration", "capability": "canSeeMedia"}, + {"id": "size", "capability": "canSeeMedia"}, + {"id": "pixels"} + ], + /* + If "tv" is set to true, then in TV mode, the site logo will be displayed in + the corner of the screen. + */ + "tv": { + "showLogo": false + }, + /* + The "user" object contains the default user settings. "ui" is the default + interface state for new users, and after selecting "Reset UI Settings" in + Preferences -> Advanced. This is the place to configure various defaults, + like the site-wide language and theme, the default list and item views, the + default set of filters, etc. + Please make sure that in case you rename or remove entries that are + referenced in "ui", you update them here as well. + */ + "user": { + "level": "guest", + "ui": { + "annotationsCalendarSize": 128, + "annotationsMapSize": 128, + "annotationsRange": "all", + "annotationsSize": 256, + "annotationsSort": "position", + "calendarFind": "", + "calendarSelection": "", + "clipColumns": 2, + "clipSize": 416, + "collectionColumns": ["title", "id", "extension", "dimensions", "size", "description", "matches", "user", "created", "modified"], + "collectionColumnWidth": {}, + "collectionSelection": [], + "collectionSort": [ + {"key": "title", "operator": "+"}, + {"key": "extension", "operator": "+"} + ], + "collectionView": "grid", + "collections": {}, + "columns": { + "Colors": { + "columns": ["title", "director", "language", "hue", "saturation", "brightness"], + "columnWidth": {} + } + }, + "document": "", + "documents": {}, + "documentSize": 256, + "documentView": "view", + "documentsSelection": {}, + "documentsSort": [{"key": "title", "operator": "+"}], + "documentsView": "grid", + "edit": "", + "edits": {}, + "editSelection": [], + "editSort": [ + {"key": "index", "operator": "+"}, + {"key": "year", "operator": "+"}, + {"key": "director", "operator": "+"}, + {"key": "title", "operator": "+"}, + {"key": "position", "operator": "+"}, + {"key": "duration", "operator": "+"} + ], + "editView": "list", + "embedSize": 256, + "entitiesSelection": {}, + "entitiesType": "", + "filters": [ + {"id": "director", "sort": [{"key": "items", "operator": "-"}]}, + {"id": "country", "sort": [{"key": "items", "operator": "-"}]}, + {"id": "year", "sort": [{"key": "name", "operator": "-"}]}, + {"id": "type", "sort": [{"key": "items", "operator": "-"}]}, + {"id": "keywords", "sort": [{"key": "items", "operator": "-"}]} + ], + "filtersSize": 176, + "find": {"conditions": [], "operator": "&"}, + "findDocuments": {"conditions": [], "operator": "&"}, + "followPlayer": true, + "help": "", + "icons": "posters", + "infoIconSize": 256, + "item": "", + "itemFind": "", + "itemSort": [{"key": "position", "operator": "+"}], + "itemView": "info", + "listColumns": ["title", "director", "year", "language", "duration"], + "listColumnWidth": {}, + "listSelection": [], + "listSort": [{"key": "title", "operator": "+"}], + "listView": "grid", + "lists": {}, + "locale": "en", + "mapFind": "", + "mapSelection": "", + "page": "", + "part": { + "api": "", + "document": "", + "entities": "", + "faq": "", + "help": "", + "news": "", + "preferences": "", + "tv": "" + }, + "section": "items", + "sequenceMode": "shape", + "sequenceSort": [{"key": "director", "operator": "+"}], + "showAdvancedEmbedOptions": false, + "showAnnotations": true, + "showAnnotationsCalendar": true, + "showAnnotationsMap": true, + "showBrowser": true, + "showCalendarControls": false, + "showClips": true, + "showDocument": true, + "showFilters": true, + "showIconBrowser": false, + "showInfo": true, + "showLayers": { + "keywords": true, + "notes": true, + "subtitles": true + }, + "showMapControls": false, + "showMapLabels": false, + "showFolder": { + "edits": { + "personal": true, + "favorite": true, + "featured": true, + "volumes": true + }, + "items": { + "personal": true, + "favorite": true, + "featured": true, + "volumes": true + }, + "documents": { + "personal": true, + "favorite": true, + "featured": true + } + }, + "showReflections": true, + "showSidebar": true, + "showSitePosters": false, + "showTimeline": true, + "sidebarSize": 256, + "text": "", + "texts": {}, + "theme": "oxmedium", + "updateAdvancedFindResults": false, + "videoLoop": false, + "videoMuted": false, + "videoPoints": {}, + "videoResolution": 240, + "videoScale": "fit", + "videoSize": "small", + "videoSubtitles": true, + "videoSubtitlesOffset": 0, + "videoTimeline": "antialias", + "videoView": "player", + "videoVolume": 1 + }, + "script": "", + "username": "", + "volumes": [] + }, + /* + "userLevels" is an ordered list of user classes. The first entry is for + unregistered visitors. + */ + "userLevels": ["guest", "member", "staff", "admin"], + /* + "video" contains the video settings. + "formats": Supported video formats. + Should be ["webm", "mp4"] to support WebM and MP4, + ["webm"] if only WebM is used or + ["mp4"] for MP4 only. + "previewRatio": Aspect ratio used in the info panel in the bottom left + corner of the screen + "resolutions": List of video resolutions. Supported values are 96, 144, + 240, 288, 360, 432, 480, 720 and 1080. + "torrent": If true, video downloads are offered via BitTorrent + */ + "video": { + "downloadFormat": "mp4", + "formats": ["webm", "mp4"], + "previewRatio": 1.77777777777777777777, + "resolutions": [240, 720], + "torrent": true + } +} \ No newline at end of file diff --git a/install.py b/install.py new file mode 100755 index 0000000..a2f5701 --- /dev/null +++ b/install.py @@ -0,0 +1,84 @@ +#!/usr/bin/python3 + +import os +from os.path import join, abspath, basename, dirname + +# change this +name = 'cinemuse' +overwrite = ( + #('home', 'indiancinema'), + #('infoView', 'indiancinema'), +) + +base = abspath(dirname(__file__)) +os.chdir(base) + +for root, folders, files in os.walk(join(base, 'static')): + for f in files: + src = join(root, f) + target = src.replace(base, '/srv/pandora') + rel_src = os.path.relpath(src, dirname(target)) + if os.path.exists(target): + os.unlink(target) + os.symlink(rel_src, target) + +if overwrite: + os.chdir('/srv/pandora/static/js') + for filename, sitename in overwrite: + src = '%s.%s.js' % (filename, sitename) + target = '%s.%s.js' % (filename, name) + if os.path.exists(target): + os.unlink(target) + os.symlink(src, target) + +os.chdir(base) +src = join(base, 'config.jsonc') +target = '/srv/pandora/pandora/config.%s.jsonc' % name +rel_src = os.path.relpath(src, dirname(target)) +if os.path.exists(target): + os.unlink(target) +os.symlink(rel_src, target) +t = '/srv/pandora/pandora/config.jsonc' +if os.path.exists(t): + os.unlink(t) +os.symlink(basename(target), t) + +for root, folders, files in os.walk(join(base, 'scripts')): + for f in files: + src = join(root, f) + target = src.replace(base, '/srv/pandora') + rel_src = os.path.relpath(src, dirname(target)) + if os.path.exists(target): + os.unlink(target) + os.symlink(rel_src, target) + if f == 'poster.%s.py' % name: + t = os.path.join(dirname(target), 'poster.py') + if os.path.exists(t): + os.unlink(t) + os.symlink(f, os.path.join(dirname(target), t)) + +if os.path.exists('__init__.py'): + # make module available to pandora + target = os.path.join('/srv/pandora/pandora/', name) + rel_src = os.path.relpath(base, dirname(target)) + if os.path.exists(target): + os.unlink(target) + os.symlink(rel_src, target) + + # include module in local settings + local_settings_py = '/srv/pandora/pandora/local_settings.py' + with open(local_settings_py) as fd: + local_settings_changed = False + local_settings = fd.read() + if 'LOCAL_APPS' not in local_settings: + local_settings += '\nLOCAL_APPS = ["%s"]\n' % name + local_settings_changed = True + else: + apps = re.compile('(LOCAL_APPS.*?)\]', re.DOTALL).findall(local_settings)[0] + if name not in apps: + new_apps = apps.strip() + ',\n"%s"\n' % name + local_settings = local_settings.replace(apps, new_apps) + local_settings_changed = True + if local_settings_changed: + with open(local_settings_py, 'w') as fd: + fd.write(local_settings) diff --git a/split_keywords.py b/split_keywords.py new file mode 100755 index 0000000..a84f273 --- /dev/null +++ b/split_keywords.py @@ -0,0 +1,63 @@ +#!/usr/bin/python3 + +import ox +import ox.web.auth +import json +import sys + +url = 'http://131.111.144.26/api/' + +api = ox.API(url) +site = '131.111.144.26' + +try: + credentials = ox.web.auth.get(site) +except: + credentials = {} + print('Please provide your username and password for %s:' % site) + credentials['username'] = input('Username: ') + credentials['password'] = getpass.getpass('Password: ') + update = True +r = api.signin(**credentials) +if 'errors' in r.get('data', {}): + for kv in r['data']['errors'].items(): + print('%s: %s' % kv) + sys.exit(1) +if update: + ox.web.auth.update(site, credentials) + +old = [] +for annotation in api.findAnnotations({ + 'query': { + 'conditions': [{ + 'key': 'value', + 'value': ', ', + 'operator': '=' + }, { + 'key': 'layer', + 'value': 'keywords', + 'operator': '==' + }], + 'operator': '&' + }, + 'keys': ['id', 'in', 'out', 'value', 'user', 'created'], + 'range': [0, 50000] +})['data']['items']: + item = annotation['id'].split('/')[0] + for v in annotation['value'].split(', '): + v = v.strip() + a = { + 'in': annotation['in'], + 'out': annotation['out'], + 'item': item, + 'layer': 'keywords', + 'value': v, + } + print(a) + r = api.addAnnotation(a) + print(r.get('status')) + api.removeAnnotation({'id': annotation['id']}) + old.append(annotation) + +with open('old.json', 'w') as fd: + json.dump(old, fd, indent=4, ensure_ascii=False) diff --git a/static/js/infoView.cinemuse.js b/static/js/infoView.cinemuse.js new file mode 100644 index 0000000..d8ce458 --- /dev/null +++ b/static/js/infoView.cinemuse.js @@ -0,0 +1,1141 @@ +'use strict'; + +pandora.ui.infoView = function(data) { + + var ui = pandora.user.ui, + canEdit = pandora.hasCapability('canEditMetadata'), + canRemove = pandora.hasCapability('canRemoveItems'), + canSeeAllMetadata = pandora.user.level != 'guest', + css = { + marginTop: '4px', + textAlign: 'justify' + }, + iconRatio = ui.icons == 'posters' ? ( + ui.showSitePosters ? pandora.site.posters.ratio : data.posterRatio + ) : 1, + iconSize = ui.infoIconSize, + iconWidth = iconRatio > 1 ? iconSize : Math.round(iconSize * iconRatio), + iconHeight = iconRatio < 1 ? iconSize : Math.round(iconSize / iconRatio), + iconLeft = iconSize == 256 ? Math.floor((iconSize - iconWidth) / 2) : 0, + borderRadius = ui.icons == 'posters' ? 0 : iconSize / 8, + isCopyrighted = !data.year || parseInt(data.year) + 60 >= new Date().getFullYear(), + listWidth = 0, + margin = 16, + // these may contain commas, and are thus separated by semicolons + specialListKeys = ['alternativeTitles', 'productionCompany'], + nameKeys = pandora.site.itemKeys.filter(function(key) { + return key.sortType == 'person'; + }).map(function(key) { + return key.id; + }), + listKeys = pandora.site.itemKeys.filter(function(key) { + return Ox.isArray(key.type) && !Ox.contains(specialListKeys, key.id); + }).map(function(key){ + return key.id; + }), + posterKeys = ['title', 'director', 'year'], + descriptions = { + names: getNames(), + studios: getStudios() + }, + statisticsWidth = 128, + + $bar = Ox.Bar({size: 16}) + .bindEvent({ + doubleclick: function(e) { + if ($(e.target).is('.OxBar')) { + $info.animate({scrollTop: 0}, 250); + } + } + }), + + $options = Ox.MenuButton({ + items: [ + { + id: 'imdb', + title: Ox._('Update IMDb ID...'), + disabled: !canEdit + }, + { + id: 'metadata', + title: Ox._('Update Metadata...'), + disabled: !canEdit + }, + {}, + { + id: 'upload', + title: Ox._('Upload Video...'), + disabled: !pandora.hasCapability('canAddItems') + }, + {}, + { + id: 'delete', + title: Ox._('Delete {0}...', [pandora.site.itemName.singular]), + disabled: !canRemove + } + ], + style: 'square', + title: 'set', + tooltip: Ox._('Options'), + type: 'image', + }) + .css({ + float: 'left', + borderColor: 'rgba(0, 0, 0, 0)', + background: 'rgba(0, 0, 0, 0)' + }) + .bindEvent({ + click: function(data_) { + if (data_.id == 'imdb') { + pandora.$ui.idDialog = pandora.ui.idDialog(data).open(); + } else if (data_.id == 'metadata') { + pandora.$ui.metadataDialog = pandora.ui.metadataDialog(data).open(); + } else if (data_.id == 'upload') { + pandora.$ui.addItemDialog = pandora.ui.addItemDialog({ + item: pandora.user.ui.item, + selected: 'upload' + }).open(); + } else if (data_.id == 'delete') { + pandora.$ui.deleteItemsDialog = pandora.ui.deleteItemsDialog({ + items: [data] + }).open(); + } + } + }) + .appendTo($bar), + + $edit = Ox.MenuButton({ + items: [ + { + id: 'insert', + title: Ox._('Insert HTML...'), + disabled: true + } + ], + style: 'square', + title: 'edit', + tooltip: Ox._('Edit'), + type: 'image', + }) + .css({ + float: 'right', + borderColor: 'rgba(0, 0, 0, 0)', + background: 'rgba(0, 0, 0, 0)' + }) + .bindEvent({ + click: function(data) { + // ... + } + }) + .appendTo($bar), + + $info = Ox.Element().css({overflowY: 'auto'}), + + that = Ox.SplitPanel({ + elements: [ + {element: $bar, size: 16}, + {element: $info} + ], + orientation: 'vertical' + }), + + $icon = Ox.Element({ + element: '' + }) + .attr({ + src: pandora.getMediaURL('/' + data.id + '/' + ( + ui.icons == 'posters' ? 'poster' : 'icon' + ) + '512.jpg?' + data.modified) + }) + .css({ + position: 'absolute', + left: margin + iconLeft + 'px', + top: margin + 'px', + width: iconWidth + 'px', + height: iconHeight + 'px', + borderRadius: borderRadius + 'px', + cursor: 'pointer' + }) + .bindEvent({ + singleclick: toggleIconSize + }) + .appendTo($info), + + $reflection = $('
') + .addClass('OxReflection') + .css({ + position: 'absolute', + left: margin + 'px', + top: margin + iconHeight + 'px', + width: iconSize + 'px', + height: Math.round(iconSize / 2) + 'px', + overflow: 'hidden' + }) + .appendTo($info), + + $reflectionIcon = $('') + .attr({ + src: pandora.getMediaURL('/' + data.id + '/' + ( + ui.icons == 'posters' + ? (ui.showSitePosters ? 'siteposter' : 'poster') : 'icon' + ) + '512.jpg?' + data.modified) + }) + .css({ + position: 'absolute', + left: iconLeft + 'px', + width: iconWidth + 'px', + height: iconHeight + 'px', + borderRadius: borderRadius + 'px' + }) + .appendTo($reflection), + + $reflectionGradient = $('
') + .css({ + position: 'absolute', + width: iconSize + 'px', + height: Math.round(iconSize / 2) + 'px' + }) + .appendTo($reflection), + + $text = Ox.Element() + .addClass('OxTextPage') + .css({ + position: 'absolute', + left: margin + (iconSize == 256 ? 256 : iconWidth) + margin + 'px', + top: margin + 'px', + right: margin + statisticsWidth + margin + 'px' + }) + .appendTo($info), + + $alternativeTitles, + + $minutes, + + $imdb, + + $links, + + $descriptions, + + $statistics = $('
') + .css({ + position: 'absolute', + width: statisticsWidth + 'px', + top: margin + 'px', + right: margin + 'px' + }) + .appendTo($info), + + $capabilities; + + [$options, $edit].forEach(function($element) { + $element.find('input').css({ + borderWidth: 0, + borderRadius: 0, + padding: '3px' + }); + }); + + listKeys.forEach(function(key) { + if (Ox.isString(data[key])) { + data[key] = [data[key]]; + } + }); + + // Title ------------------------------------------------------------------- + + $('
') + .css({ + marginTop: '-2px' + }) + .append( + Ox.EditableContent({ + clickLink: pandora.clickLink, + editable: canEdit, + format: function(value) { + return formatTitle(value); + }, + tooltip: canEdit ? pandora.getEditTooltip() : '', + value: data.title + }) + .css({ + marginBottom: '-3px', + fontWeight: 'bold', + fontSize: '13px' + }) + .bindEvent({ + submit: function(event) { + editMetadata('title', event.value); + } + }) + ) + .appendTo($text); + + // Director ---------------------------------------------------------------- + + if (data.director || canEdit) { + $('
') + .css({ + marginTop: '2px' + }) + .append( + Ox.EditableContent({ + clickLink: pandora.clickLink, + editable: canEdit, + format: function(value) { + return formatLink(value.split(', '), 'name'); + }, + placeholder: formatLight(Ox._('Unknown Director')), + tooltip: canEdit ? pandora.getEditTooltip() : '', + value: data.director ? data.director.join(', ') : '' + }) + .css({ + marginBottom: '-3px', + fontWeight: 'bold', + fontSize: '13px' + }) + .bindEvent({ + submit: function(event) { + editMetadata('director', event.value); + } + }) + ) + .appendTo($text); + } + + // Groups ------------------------------------------------------------------ + + renderGroup(['alternativeTitles']); + + renderGroup(['country', 'year', 'language', 'runtime', 'color', 'sound']); + + renderGroup(['productionCompany']); + + renderGroup([ + 'producer', 'codirector', 'writer', 'cinematographer', 'editor', + 'composer', 'lyricist' + ]); + + renderGroup(['actor']); + + renderGroup(canSeeAllMetadata ? ['genre', 'topic'] : ['genre']); + + renderGroup([ + 'censorshipcertificatenumber', + 'dateofcensorcertificate', + 'ratingcertificate', + 'length', + 'numberofreels', + 'format', + 'releasedate' + ]); + + renderGroup(['imdbId', 'links']); + + if (canEdit) { + updateIMDb(); + } + + // Encyclopedia and Wiki --------------------------------------------------- + + if (['staff', 'admin'].indexOf(pandora.user.level) > -1 && canEdit) { + renderGroup(['encyclopedia', 'wiki']); + } + + // Summary ----------------------------------------------------------------- + + if (data.summary || canEdit) { + Ox.EditableContent({ + clickLink: pandora.clickLink, + collapseToEnd: false, + editable: canEdit, + format: function(value) { + return value.replace( + /').attr({id: 'descriptions'}).appendTo($text); + + renderDescriptions(); + + $('
').css({height: '16px'}).appendTo($text); + + if (data.parts && data.rendered) { + + // Annotations --------------------------------------------------------- + + $div = $('
') + .css({marginBottom: '4px'}) + .append(formatKey( + 'Annotations', 'link', '/' + data['id'] + '/' + ui.videoView + )) + .append( + Ox.Theme.formatColor(null, 'gradient') + .css({textAlign: 'right'}) + .html(Ox.formatNumber(data.numberofannotations || 0)) + ) + .appendTo($statistics); + + pandora.createLinks($div); + + // Duration, Aspect Ratio ---------------------------------------------- + + ['duration', 'aspectratio'].forEach(function(key) { + var itemKey = Ox.getObjectById(pandora.site.itemKeys, key), + value = data[key] || 0; + $('
') + .css({marginBottom: '4px'}) + .append(formatKey(itemKey.title, 'statistics')) + .append( + Ox.Theme.formatColor(null, 'gradient') + .css({textAlign: 'right'}) + .html( + Ox['format' + Ox.toTitleCase(itemKey.format.type)] + .apply(null, [value].concat(itemKey.format.args)) + ) + ) + .appendTo($statistics); + }); + + // Hue, Saturation, Lightness, Volume ---------------------------------- + + ['hue', 'saturation', 'lightness', 'volume'].forEach(function(key) { + var value = data[key] || 0; + $('
') + .css({marginBottom: '4px'}) + .append(formatKey(key, 'statistics')) + .append( + Ox.Theme.formatColor(value, key == 'volume' ? 'lightness' : key) + .css({textAlign: 'right'}) + ) + .appendTo($statistics); + }); + + // Cuts per Minute, Words per Minute ----------------------------------- + + ['cutsperminute', 'wordsperminute'].forEach(function(key) { + var value = data[key] || 0; + $('
') + .css({marginBottom: '4px'}) + .append( + formatKey(Ox.toTitleCase(key.slice(0, -9)) + ' per Minute', 'statistics') + ) + .append( + Ox.Theme.formatColor(null, 'gradient') + .css({textAlign: 'right'}) + .html(Ox.formatNumber(value, 3)) + ) + .appendTo($statistics); + }); + + } else { + // no video placeholder + } + + // Rights Level ------------------------------------------------------------ + + var $rightsLevel = $('
'); + var $div = $('
') + .css({marginBottom: '4px'}) + .append(formatKey('Rights Level', 'link', '/copyrights')) + .append($rightsLevel) + .appendTo($statistics); + pandora.createLinks($div); + renderRightsLevel(); + + // Comments ---------------------------------------------------------------- + + if (canEdit) { + $('
') + .css({marginBottom: '4px'}) + .append( + formatKey('Comments', 'statistics').options({ + tooltip: Ox._('Only {0} can see and edit these comments', [ + Object.keys(pandora.site.capabilities.canEditMetadata).map(function(level, i) { + return ( + i == 0 ? '' + : i < Ox.len(pandora.site.capabilities.canEditMetadata) - 1 ? ', ' + : ' ' + Ox._('and') + ' ' + ) + Ox.toTitleCase(level) + }).join('')]) + }) + ) + .append( + Ox.EditableContent({ + clickLink: pandora.clickLink, + placeholder: formatLight(Ox._('No comments')), + tooltip: pandora.getEditTooltip(), + type: 'textarea', + value: data.comments || '', + width: 128 + }) + .bindEvent({ + submit: function(event) { + pandora.api.edit({ + id: data.id, + comments: event.value + }, function(result) { + // ... + }); + } + }) + ) + .appendTo($statistics); + } + + $('
').css({height: '16px'}).appendTo($statistics); + + function editMetadata(key, value) { + if (value != data[key]) { + var edit = {id: data.id}; + if (key == 'alternativeTitles') { + edit[key] = value ? Ox.decodeHTMLEntities(value).split('; ').map(function(value) { + return [Ox.encodeHTMLEntities(value), []]; + }) : []; + data[key] = edit[key]; + $alternativeTitles.html(formatKey(key)); + } else if (key == 'year') { + edit[key] = value ? parseInt(value) : ''; + } else if (key == 'runtime') { + edit[key] = value ? parseInt(value) * 60 : ''; + $minutes[value ? 'show' : 'hide'](); + } else if (listKeys.indexOf(key) > -1) { + edit[key] = value ? value.split(', ') : []; + } else if (specialListKeys.indexOf(key) > -1) { + edit[key] = value + ? Ox.decodeHTMLEntities(value).split('; ').map(Ox.encodeHTMLEntities) + : []; + } else { + edit[key] = value; + } + pandora.api.edit(edit, function(result) { + var src; + data[key] = result.data[key]; + if (result.data.id != data.id) { + Ox.Request.clearCache(); // fixme: too much + pandora.UI.set({item: result.data.id}); + pandora.$ui.browser.value(data.id, 'id', result.data.id); + } else { + Ox.Request.clearCache('autocomplete'); + } + pandora.updateItemContext(); + pandora.$ui.browser.value(result.data.id, key, result.data[key]); + if (Ox.contains(posterKeys, key) && ui.icons == 'posters') { + src = pandora.getMediaURL('/' + data.id + '/poster512.jpg?' + Ox.uid()); + $icon.attr({src: src}); + $reflectionIcon.attr({src: src}); + } + if (Ox.contains(nameKeys, key)) { + data['namedescription'] = result.data['namedescription']; + descriptions.names = getNames(); + renderDescriptions(); + } else if (key == 'productionCompany') { + data['productionCompanydescription'] = result.data['productionCompanydescription']; + descriptions.studios = getStudios(); + renderDescriptions(); + } else if (key == 'imdbId') { + updateIMDb(); + } + }); + } + } + + function formatKey(key, mode, link) { + var item = Ox.getObjectById(pandora.site.itemKeys, key); + key = Ox._(item ? item.title : key); + mode = mode || 'text'; + if (key == 'alternativeTitles') { + key = Ox._('Alternative Title' + ( + data.alternativeTitles && data.alternativeTitles.length == 1 ? '' : 's' + )); + } else if (key == 'keyword') { + key = 'keywords' + } + var value = Ox.toTitleCase(key) + .replace(' Of ', ' of ') + .replace(' Per ', ' per ') + return mode == 'text' + ? '' + value + ': ' + : mode == 'description' + ? value + : mode == 'link' + ? Ox.Element() + .css({marginBottom: '4px', fontWeight: 'bold'}) + .html('' + value + '') + : Ox.Element() + .css({marginBottom: '4px', fontWeight: 'bold'}) + .html(value); + } + + function formatLight(str) { + return '' + str + ''; + } + + function formatLink(value, key) { + return (Ox.isArray(value) ? value : [value]).map(function(value) { + return key + ? '' + value + '' + : value; + }).join(Ox.contains(specialListKeys, key) ? '; ' : ', '); + } + + function formatTitle(title) { + var match = /(.+) (\(S\d{2}(E\d{2})?\))/.exec(title); + if (match) { + title = formatLink(match[1], 'title') + ' ' + + formatLight(match[2]) + + title.substr(match[0].length); + } + return title + ( + data.originalTitle && data.originalTitle != title + ? ' ' + formatLight('(' + data.originalTitle + ')') : '' + ); + } + + function formatValue(key, value) { + var ret; + if (key == 'year') { + ret = formatLink(value, 'year'); + } else if (['releasedate', 'dateofcensorcertificate'].indexOf(key) > -1) { + ret = value ? Ox.formatDate(value, + ['', '%Y', '%B %Y', '%B %e, %Y'][value.split('-').length], + true + ) : ''; + } else if (key == 'links') { + ret = value.split(', ').map(function(link) { + return '' + Ox.parseURL(link).host + ''; + }).join(', '); + + } else if (nameKeys.indexOf(key) > -1) { + ret = formatLink(value.split(', '), 'name'); + } else if (listKeys.indexOf(key) > -1) { + ret = formatLink(value.split(', '), key); + } else if (specialListKeys.indexOf(key) > -1) { + ret = formatLink( + Ox.decodeHTMLEntities(value).split('; ').map(Ox.encodeHTMLEntities), + key + ); + } else if (key == 'imdbId') { + ret = '' + value + ''; + } else if (key == 'encyclopedia') { + ret = '' + + value + ''; + } else if (key == 'wiki') { + ret = '' + + Ox.decodeURIComponent(value.split('wiki/').pop()) + ''; + } else { + ret = value; + } + return ret; + } + + function getFilmography(key, value, roles, callback) { + var keys = ['id', 'title', 'year'].concat( + key == 'name' ? nameKeys : [] + ); + pandora.api.find({ + keys: keys, + query: { + conditions: [{key: key, operator: '==', value: value}], + operator: '&' + }, + sort: [ + {key: 'year', operator: '+'}, + {key: 'title', operator: '+'}, + {key: 'director', operator: '+'} + ], + range: [0, 1000000] + }, function(result) { + var $element = Ox.Element(''), + items = {}; + if (result.data.items) { + result.data.items.forEach(function(item) { + var year = item.year || Ox._('Unknown Year'); + if (key == 'name' && result.data.items.length > 1) { + item.roles = nameKeys.filter(function(nameKey) { + return item[nameKey] && Ox.contains(item[nameKey], value); + }); + if (roles.length == 1 && Ox.isEqual(item.roles, roles)) { + delete item.roles; + } else { + item.roles = item.roles.map(function(nameKey) { + return Ox.getObjectById(pandora.site.itemKeys, nameKey).title; + }); + } + } + if (!items[year]) { + items[year] = []; + } + items[year].push(item); + }); + } + $element.html( + Object.keys(items).sort().map(function(year) { + return '' + year + ': ' + items[year].map(function(item) { + return '' + item.title + '' + + (item.roles ? ' (' + item.roles.join(', ') + ')' : ''); + }).join(', '); + }).join(', ') + ); + pandora.createLinks($element); + callback($element); + }); + } + + function getNames() { + var names = []; + nameKeys.forEach(function(key) { + data[key] && data[key].forEach(function(name) { + var index = Ox.indexOf(names, function(value) { + return value.name == name; + }); + if (index == -1) { + names.push({ + name: name, + keys: [key], + description: data.namedescription + ? data.namedescription[name] + : void 0 + }); + } else { + names[index].keys.push(key); + } + }); + }); + return names; + } + + function getRightsLevelElement(rightsLevel) { + return Ox.Theme.formatColorLevel( + rightsLevel, + pandora.site.rightsLevels.map(function(rightsLevel) { + return Ox._(rightsLevel.name); + }) + ); + } + + function getStudios() { + var studios = []; + if (data.productionCompany) { + data.productionCompany.forEach(function(studio) { + studios.push({ + name: studio, + keys: ['productionCompany'], + description: data.productionCompanydescription + ? data.productionCompanydescription[studio] + : void 0 + }); + }); + } + return studios; + } + + function getValue(key, value) { + return !value ? '' + : key == 'alternativeTitles' ? value.map(function(value) { + return value[0]; + }).join('; ') + : key == 'runtime' ? Math.round(value / 60) + : Ox.contains(listKeys, key) ? value.join(', ') + : Ox.contains(specialListKeys, key) ? value.join('; ') + : value; + } + + function renderCapabilities(rightsLevel) { + var capabilities = [].concat( + canEdit ? [{name: 'canSeeItem', symbol: 'Find'}] : [], + [ + {name: 'canPlayClips', symbol: 'PlayInToOut'}, + {name: 'canPlayVideo', symbol: 'Play'}, + {name: 'canDownloadVideo', symbol: 'Download'} + ] + ), + userLevels = canEdit ? pandora.site.userLevels : [pandora.user.level]; + $capabilities.empty(); + userLevels.forEach(function(userLevel, i) { + var $element, + $line = $('
') + .css({ + height: '16px', + marginBottom: '4px' + }) + .appendTo($capabilities); + if (canEdit) { + $element = Ox.Theme.formatColorLevel(i, userLevels.map(function(userLevel) { + return Ox._(Ox.toTitleCase(userLevel)); + }), [0, 240]); + Ox.Label({ + textAlign: 'center', + title: Ox._(Ox.toTitleCase(userLevel)), + width: 60 + }) + .addClass('OxColor OxColorGradient') + .css({ + float: 'left', + height: '12px', + paddingTop: '2px', + background: $element.css('background'), + fontSize: '8px', + color: $element.css('color') + }) + .data({OxColor: $element.data('OxColor')}) + .appendTo($line); + } + capabilities.forEach(function(capability) { + var hasCapability = pandora.hasCapability(capability.name, userLevel) >= rightsLevel, + $element = Ox.Theme.formatColorLevel(hasCapability, ['', '']); + Ox.Button({ + tooltip: Ox._('{0} ' + + (hasCapability ? 'can' : 'can\'t') + ' ' + + Ox.toSlashes(capability.name) + .split('/').slice(1).join(' ') + .toLowerCase() + .replace('see item', 'see the item') + .replace('play video', 'play the full video') + .replace('download video', 'download the video'), + [canEdit ? Ox._(Ox.toTitleCase(userLevel)) : Ox._('You')]), + title: capability.symbol, + type: 'image' + }) + .addClass('OxColor OxColorGradient') + .css({background: $element.css('background')}) + .css('margin' + (canEdit ? 'Left' : 'Right'), '4px') + .data({OxColor: $element.data('OxColor')}) + .appendTo($line); + }); + if (!canEdit) { + Ox.Button({ + title: Ox._('Help'), + tooltip: Ox._('About Rights'), + type: 'image' + }) + .css({marginLeft: '52px'}) + .bindEvent({ + click: function() { + pandora.UI.set({page: 'copyrights'}); + } + }) + .appendTo($line); + } + }); + } + + function renderDescriptions() { + $descriptions.empty(); + ['studios', 'names'].forEach(function(key) { + descriptions[key].forEach(function(value) { + if (canEdit || value.description) { + var filmography = key == 'studios' ? Ox._('Films') : Ox._('Filmography'), + $name = Ox.Element() + .addClass('OxSelectable') + .css(css) + .css({marginTop: '12px', fontWeight: 'bold'}) + .html( + formatLink( + value.name, + key == 'studios' ? 'productionCompany' : 'name' + ) + ' (' + value.keys.map(function(key) { + return formatKey(key, 'description'); + }).join(', ') + ') - ' + ) + .appendTo($descriptions), + $link = $('') + .addClass('OxLink') + .css({fontWeight: 'bold'}) + .html(Ox._('Show {0}', [filmography])) + .one({ + click: function() { + $link.removeClass('OxLink') + .html(Ox._('Loading {0}...', [filmography])); + getFilmography( + key == 'studios' ? 'productionCompany' : 'name', + Ox.decodeHTMLEntities(value.name), + value.keys, + function($element) { + $link.addClass('OxLink') + .html(Ox._('Hide {0}', [filmography])) + .on({ + click: function() { + if (Ox.startsWith($link.html(), Ox._('Show'))) { + $link.html(Ox._('Hide {0}', [filmography])); + $text.show(); + } else { + $link.html(Ox._('Show {0}', [filmography])); + $text.hide(); + } + } + }); + $text.append($element).show(); + } + ); + } + }) + .appendTo($name), + $text = $('
') + .addClass('OxSelectable') + .css(css) + .hide() + .appendTo($descriptions); + pandora.createLinks($name); + Ox.EditableContent({ + clickLink: pandora.clickLink, + editable: canEdit, + format: function(value) { + return value.replace( + /').addClass('OxSelectable').css(css); + keys.forEach(function(key, i) { + if (canEdit || data[key]) { + if ($element.children().length) { + $('').html('; ').appendTo($element); + } + if (key == 'alternativeTitles') { + $alternativeTitles = $('') + .html(formatKey(key)) + .appendTo($element); + } else { + $('') + .html(formatKey(key)) + .appendTo($element); + } + Ox.EditableContent({ + clickLink: pandora.clickLink, + format: function(value) { + return formatValue(key, value); + }, + placeholder: formatLight(Ox._('unknown')), + tooltip: canEdit ? pandora.getEditTooltip() : '', + value: getValue(key, data[key]) + }) + .bindEvent({ + edit: function() { + key == 'runtime' && $minutes.show(); + }, + submit: function(data) { + if (key == 'encyclopedia') { + data.value = ['Index', 'Summary'].indexOf(data.value) > -1 + ? data.value + : ''; + this.options({ + value: data.value + }); + } + + editMetadata(key, data.value); + } + }) + .appendTo($element); + if (key == 'runtime') { + $minutes = $('') + .html(' min') + [data.runtime ? 'show' : 'hide']() + .appendTo($element); + } else if (key == 'imdbId') { + $imdb = Ox.Element('') + .appendTo($element); + pandora.createLinks($imdb); + } + } + }); + $element.appendTo($text); + } + return $element; + } + + function renderRightsLevel() { + var $rightsLevelElement = getRightsLevelElement(data.rightslevel), + $rightsLevelSelect; + $rightsLevel.empty(); + if (canEdit) { + $rightsLevelSelect = Ox.Select({ + items: pandora.site.rightsLevels.map(function(rightsLevel, i) { + return { + id: i, + title: Ox._(rightsLevel.name), + disabled: !isCopyrighted && rightsLevel.name == 'Under Copyright' + || isCopyrighted && rightsLevel.name == 'Out of Copyright' + }; + }), + width: 128, + value: data.rightslevel + }) + .addClass('OxColor OxColorGradient') + .css({ + marginBottom: '4px', + background: $rightsLevelElement.css('background') + }) + .data({OxColor: $rightsLevelElement.data('OxColor')}) + .bindEvent({ + change: function(event) { + var rightsLevel = event.value; + $rightsLevelElement = getRightsLevelElement(rightsLevel); + $rightsLevelSelect + .css({background: $rightsLevelElement.css('background')}) + .data({OxColor: $rightsLevelElement.data('OxColor')}) + // renderCapabilities(rightsLevel); + pandora.api.edit({id: data.id, rightslevel: rightsLevel}, function(result) { + // ... + }); + } + }) + .appendTo($rightsLevel); + } else { + $rightsLevelElement + .css({ + marginBottom: '4px' + }) + .appendTo($rightsLevel); + } + /* + if (data.parts) { + $capabilities = $('
').appendTo($rightsLevel); + renderCapabilities(data.rightslevel); + } + */ + } + + function toggleIconSize() { + iconSize = iconSize == 256 ? 512 : 256; + iconWidth = iconRatio > 1 ? iconSize : Math.round(iconSize * iconRatio); + iconHeight = iconRatio < 1 ? iconSize : Math.round(iconSize / iconRatio); + iconLeft = iconSize == 256 ? Math.floor((iconSize - iconWidth) / 2) : 0, + borderRadius = ui.icons == 'posters' ? 0 : iconSize / 8; + $icon.animate({ + left: margin + iconLeft + 'px', + width: iconWidth + 'px', + height: iconHeight + 'px', + borderRadius: borderRadius + 'px' + }, 250); + $reflection.animate({ + top: margin + iconHeight + 'px', + width: iconSize + 'px', + height: iconSize / 2 + 'px' + }, 250); + $reflectionIcon.animate({ + left: iconLeft + 'px', + width: iconWidth + 'px', + height: iconHeight + 'px', + borderRadius: borderRadius + 'px' + }, 250); + $reflectionGradient.animate({ + width: iconSize + 'px', + height: iconSize / 2 + 'px' + }, 250); + $text.animate({ + left: margin + (iconSize == 256 ? 256 : iconWidth) + margin + 'px' + }, 250); + pandora.UI.set({infoIconSize: iconSize}); + } + + function updateIMDb() { + if (data.imdbId) { + pandora.api.find({ + query: { + conditions: [{key: 'imdbId', operator: '==', value: data.imdbId}] + } + }, function(result) { + if (result.data.items == 1) { + $imdb.empty(); + } else { + $imdb.html( + ' (' + + result.data.items + ' ' + + pandora.site.itemName.plural.toLowerCase() + + ' ' + Ox._('with the same IMDb ID') + ')' + ); + } + }); + } else { + $imdb.empty(); + } + } + + that.reload = function() { + var src = pandora.getMediaURL('/' + data.id + '/' + ( + ui.icons == 'posters' ? 'poster' : 'icon' + ) + '512.jpg?' + Ox.uid()); + $icon.attr({src: src}); + $reflectionIcon.attr({src: src}); + iconSize = iconSize == 256 ? 512 : 256; + iconRatio = ui.icons == 'posters' + ? (ui.showSitePosters ? pandora.site.posters.ratio : data.posterRatio) : 1; + toggleIconSize(); + }; + + that.resizeElement = function() { + // overwrite splitpanel resize + }; + + that.bindEvent({ + mousedown: function() { + setTimeout(function() { + !Ox.Focus.focusedElementIsInput() && that.gainFocus(); + }); + }, + pandora_icons: that.reload, + pandora_showsiteposters: function() { + ui.icons == 'posters' && that.reload(); + } + }); + + return that; + +};