├── Website ├── public │ ├── js │ │ ├── index.html │ │ ├── ie10-viewport-bug-workaround.min.js │ │ ├── html5shiv.min.js │ │ ├── respond.min.js │ │ ├── MediaPlayer.min.js │ │ └── custom.js │ ├── robots.txt │ ├── css │ │ ├── index.html │ │ ├── ie10-viewport-bug-workaround.min.css │ │ ├── normalize.min.css │ │ ├── custom.css │ │ └── contribute.css │ ├── img │ │ ├── index.html │ │ ├── logo-128.png │ │ ├── logo-256.png │ │ ├── logo-32.png │ │ ├── logo-512.png │ │ ├── logo-64.png │ │ ├── logo-1024.pdn │ │ └── logo-1024.png │ └── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 ├── .gitignore ├── backups │ └── .gitignore ├── storage │ ├── app │ │ └── .gitignore │ └── framework │ │ └── views │ │ └── cache │ │ └── .gitignore ├── config │ ├── .gitignore │ └── .env.example ├── views │ ├── includes │ │ ├── progress_bar_class_by_severity.html │ │ ├── scripts.html │ │ ├── flash │ │ │ └── bootstrap-v3.html │ │ ├── old_password_1x.html │ │ ├── new_password_2x.html │ │ ├── annotation_list_group_item_content.html │ │ ├── page_bottom.html │ │ ├── page_header.html │ │ ├── modals │ │ │ └── contribute │ │ │ │ ├── select_topic.html │ │ │ │ ├── select_severity.html │ │ │ │ ├── select_channel.html │ │ │ │ ├── select_category.html │ │ │ │ └── select_next_action.html │ │ └── page_top.html │ ├── mail │ │ ├── en-US │ │ │ ├── includes │ │ │ │ ├── header.txt │ │ │ │ └── footer.txt │ │ │ ├── forgot-password.txt │ │ │ ├── confirm-email.txt │ │ │ ├── password-changed.txt │ │ │ ├── sign-up.txt │ │ │ └── email-changed.txt │ │ └── de-DE │ │ │ ├── includes │ │ │ ├── header.txt │ │ │ └── footer.txt │ │ │ ├── forgot-password.txt │ │ │ ├── confirm-email.txt │ │ │ ├── password-changed.txt │ │ │ ├── sign-up.txt │ │ │ └── email-changed.txt │ ├── privacy_policy.html │ ├── 404.html │ ├── contact.html │ ├── mcf │ │ └── example.mcf │ ├── help.html │ ├── reset_password.html │ ├── preferences.html │ ├── forgot_password.html │ ├── resend_confirmation.html │ ├── works_add_step_1.html │ ├── browse_list.html │ ├── browse_which.html │ ├── view_multiple.html │ ├── preferences_by_topic.html │ ├── sign-up.html │ ├── topic_in_work.html │ ├── annotation.html │ ├── welcome.html │ ├── work_delete.html │ ├── view_single.html │ ├── contribute.html │ ├── settings.html │ └── works_add_step_2.html ├── composer.json ├── .editorconfig ├── app │ ├── Lib │ │ ├── Throwables │ │ │ ├── Exception.php │ │ │ └── EmptyTimingException.php │ │ ├── FilterableAnnotation.php │ │ ├── UnfilterableAnnotation.php │ │ ├── Mcf │ │ │ ├── Throwables │ │ │ │ ├── EmptyContainerException.php │ │ │ │ ├── InvalidContentException.php │ │ │ │ ├── InvalidVersionException.php │ │ │ │ ├── InvalidContainerException.php │ │ │ │ ├── InvalidAnnotationException.php │ │ │ │ ├── InvalidFileEndTimeException.php │ │ │ │ ├── InvalidWebvttTimingException.php │ │ │ │ ├── InvalidFileStartTimeException.php │ │ │ │ └── InvalidWebvttTimestampException.php │ │ │ ├── WebvttTiming.php │ │ │ ├── Version.php │ │ │ ├── Annotation.php │ │ │ ├── WebvttTimestamp.php │ │ │ ├── Content.php │ │ │ └── Mcf.php │ │ ├── Imdb.php │ │ ├── Playlist │ │ │ ├── Timestamp.php │ │ │ ├── FilePlaylist.php │ │ │ ├── PlaylistItem.php │ │ │ └── Playlist.php │ │ ├── Annotation.php │ │ ├── Xml.php │ │ ├── Edl.php │ │ ├── ContentualAnnotation.php │ │ ├── M3u.php │ │ ├── Timing.php │ │ ├── Xspf.php │ │ ├── Timestamp.php │ │ └── Filter.php │ ├── Controller.php │ ├── AnnotationViewerTrait.php │ ├── BrowsingController.php │ ├── EmailSenderTrait.php │ ├── index.php │ ├── PrefsController.php │ └── SettingsController.php ├── maintenance.php ├── index.php ├── .htaccess └── deploy.sh ├── .editorconfig ├── Database └── .editorconfig └── README.md /Website/public/js/index.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Website/public/robots.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Website/public/css/index.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Website/public/img/index.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Website/.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ 2 | .idea/ 3 | 4 | # Composer 5 | vendor/ 6 | composer.phar 7 | -------------------------------------------------------------------------------- /Website/backups/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything except this file itself 2 | * 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /Website/storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything except this file itself 2 | * 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /Website/storage/framework/views/cache/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything except this file itself 2 | * 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /Website/public/img/logo-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delight-im/MovieContentFilter/HEAD/Website/public/img/logo-128.png -------------------------------------------------------------------------------- /Website/public/img/logo-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delight-im/MovieContentFilter/HEAD/Website/public/img/logo-256.png -------------------------------------------------------------------------------- /Website/public/img/logo-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delight-im/MovieContentFilter/HEAD/Website/public/img/logo-32.png -------------------------------------------------------------------------------- /Website/public/img/logo-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delight-im/MovieContentFilter/HEAD/Website/public/img/logo-512.png -------------------------------------------------------------------------------- /Website/public/img/logo-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delight-im/MovieContentFilter/HEAD/Website/public/img/logo-64.png -------------------------------------------------------------------------------- /Website/public/img/logo-1024.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delight-im/MovieContentFilter/HEAD/Website/public/img/logo-1024.pdn -------------------------------------------------------------------------------- /Website/public/img/logo-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delight-im/MovieContentFilter/HEAD/Website/public/img/logo-1024.png -------------------------------------------------------------------------------- /Website/public/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delight-im/MovieContentFilter/HEAD/Website/public/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /Website/config/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore the private version of the configuration 2 | # Keep a public and up-to-date template in '.env.example' 3 | .env 4 | -------------------------------------------------------------------------------- /Website/public/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delight-im/MovieContentFilter/HEAD/Website/public/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /Website/public/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delight-im/MovieContentFilter/HEAD/Website/public/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /Website/public/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delight-im/MovieContentFilter/HEAD/Website/public/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /Website/public/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delight-im/MovieContentFilter/HEAD/Website/public/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /Website/views/includes/progress_bar_class_by_severity.html: -------------------------------------------------------------------------------- 1 | {% if severity == 'low' %} progress-bar-warning{% elseif severity == 'medium' %} progress-bar-caution{% elseif severity == 'high' %} progress-bar-danger{% endif %} 2 | -------------------------------------------------------------------------------- /Website/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "php": ">=5.6.0", 4 | "delight-im/foundation-core": "^4.0", 5 | "delight-im/privacy-policy": "^2.0" 6 | }, 7 | "autoload": { 8 | "psr-4": { 9 | "App\\": "app/" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = tab 7 | trim_trailing_whitespace = true 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | indent_style = space 13 | indent_size = 4 14 | -------------------------------------------------------------------------------- /Database/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = tab 7 | trim_trailing_whitespace = true 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | indent_style = space 13 | indent_size = 4 14 | -------------------------------------------------------------------------------- /Website/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = tab 7 | trim_trailing_whitespace = true 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | indent_style = space 13 | indent_size = 4 14 | -------------------------------------------------------------------------------- /Website/views/mail/en-US/includes/header.txt: -------------------------------------------------------------------------------- 1 | {# 2 | * PHP-Foundation (https://github.com/delight-im/PHP-Foundation) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the MIT License (https://opensource.org/licenses/MIT) 5 | #} 6 | {% if recipientName %}Dear {{ recipientName|raw }}{% else %}Hello{% endif %}, 7 | -------------------------------------------------------------------------------- /Website/views/mail/de-DE/includes/header.txt: -------------------------------------------------------------------------------- 1 | {# 2 | * PHP-Foundation (https://github.com/delight-im/PHP-Foundation) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the MIT License (https://opensource.org/licenses/MIT) 5 | #} 6 | {% if recipientName %}Hallo {{ recipientName|raw }}{% else %}Hallo{% endif %}, 7 | -------------------------------------------------------------------------------- /Website/public/css/ie10-viewport-bug-workaround.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * IE10 viewport hack for Surface/desktop Windows 8 bug 3 | * Copyright 2014-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | @-ms-viewport{width:device-width}@-o-viewport{width:device-width}@viewport{width:device-width} 8 | -------------------------------------------------------------------------------- /Website/app/Lib/Throwables/Exception.php: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Website/public/js/ie10-viewport-bug-workaround.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * IE10 viewport hack for Surface/desktop Windows 8 bug 3 | * Copyright 2014-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | !function(){"use strict";if(navigator.userAgent.match(/IEMobile\/10\.0/)){var a=document.createElement("style");a.appendChild(document.createTextNode("@-ms-viewport{width:auto!important}")),document.querySelector("head").appendChild(a)}}(); 8 | -------------------------------------------------------------------------------- /Website/views/mail/en-US/forgot-password.txt: -------------------------------------------------------------------------------- 1 | {# 2 | * PHP-Foundation (https://github.com/delight-im/PHP-Foundation) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the MIT License (https://opensource.org/licenses/MIT) 5 | #} 6 | {% include 'mail/en-US/includes/header.txt' %} 7 | 8 | In order to reset your password for {{ projectName|raw }}, please open the link below: 9 | 10 | {{ resetUrl|raw }} 11 | 12 | You will then be able to choose a new password for your account. 13 | 14 | If you have any questions, please feel free to ask us for help. 15 | 16 | {% include 'mail/en-US/includes/footer.txt' %} 17 | -------------------------------------------------------------------------------- /Website/app/Lib/Imdb.php: -------------------------------------------------------------------------------- 1 | 9 | 12 | {{ message }} 13 | 14 | {% endfor %} 15 | {% endif %} 16 | -------------------------------------------------------------------------------- /Website/views/includes/old_password_1x.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |

6 | Please enter your{% if isReset %} old{% endif %} password to verify ownership of your account. This prevents others from locking you out of your account, even if they somehow got access to it for a short time. 7 |

8 |
9 |
10 | -------------------------------------------------------------------------------- /Website/views/mail/de-DE/forgot-password.txt: -------------------------------------------------------------------------------- 1 | {# 2 | * PHP-Foundation (https://github.com/delight-im/PHP-Foundation) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the MIT License (https://opensource.org/licenses/MIT) 5 | #} 6 | {% include 'mail/de-DE/includes/header.txt' %} 7 | 8 | um dein Passwort für {{ projectName|raw }} zurückzusetzen, öffne bitte den folgenden Link: 9 | 10 | {{ resetUrl|raw }} 11 | 12 | Du kannst anschließend ein neues Passwort für dein Benutzerkonto festlegen. 13 | 14 | Wenn du Fragen hast, kannst du uns gerne um Hilfe bitten. 15 | 16 | {% include 'mail/de-DE/includes/footer.txt' %} 17 | -------------------------------------------------------------------------------- /Website/views/mail/en-US/confirm-email.txt: -------------------------------------------------------------------------------- 1 | {# 2 | * PHP-Foundation (https://github.com/delight-im/PHP-Foundation) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the MIT License (https://opensource.org/licenses/MIT) 5 | #} 6 | {% include 'mail/en-US/includes/header.txt' %} 7 | 8 | In order to confirm your email address for {{ projectName|raw }}, please open the link below: 9 | 10 | {{ confirmationUrl|raw }} 11 | 12 | After completing the confirmation, you can sign in with this email address and your password. 13 | 14 | If you have any questions, please feel free to ask us for help. 15 | 16 | {% include 'mail/en-US/includes/footer.txt' %} 17 | -------------------------------------------------------------------------------- /Website/app/Lib/Playlist/Timestamp.php: -------------------------------------------------------------------------------- 1 | seconds, 3, '.', ''); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /Website/views/mail/de-DE/confirm-email.txt: -------------------------------------------------------------------------------- 1 | {# 2 | * PHP-Foundation (https://github.com/delight-im/PHP-Foundation) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the MIT License (https://opensource.org/licenses/MIT) 5 | #} 6 | {% include 'mail/de-DE/includes/header.txt' %} 7 | 8 | um deine E-Mail-Adresse für {{ projectName|raw }} zu bestätigen, öffne bitte den folgenden Link: 9 | 10 | {{ confirmationUrl|raw }} 11 | 12 | Nachdem du die Bestätigung abgeschlossen hast, kannst du dich mit dieser E-Mail-Adresse und deinem Passwort anmelden. 13 | 14 | Wenn du Fragen hast, kannst du uns gerne um Hilfe bitten. 15 | 16 | {% include 'mail/de-DE/includes/footer.txt' %} 17 | -------------------------------------------------------------------------------- /Website/views/mail/en-US/password-changed.txt: -------------------------------------------------------------------------------- 1 | {# 2 | * PHP-Foundation (https://github.com/delight-im/PHP-Foundation) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the MIT License (https://opensource.org/licenses/MIT) 5 | #} 6 | {% include 'mail/en-US/includes/header.txt' %} 7 | 8 | We wanted to let you know that your password for {{ projectName|raw }} has recently been changed. 9 | 10 | From now on, please use the new password to sign in to your user account. 11 | 12 | If you didn’t make this change, please contact us at our email address that you find below. 13 | 14 | Feel free to ask us for help if you have any other questions as well. 15 | 16 | {% include 'mail/en-US/includes/footer.txt' %} 17 | -------------------------------------------------------------------------------- /Website/views/mail/en-US/sign-up.txt: -------------------------------------------------------------------------------- 1 | {# 2 | * PHP-Foundation (https://github.com/delight-im/PHP-Foundation) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the MIT License (https://opensource.org/licenses/MIT) 5 | #} 6 | {% include 'mail/en-US/includes/header.txt' %} 7 | 8 | Welcome to {{ projectName|raw }}! 9 | 10 | {%if confirmationUrl %}Please confirm your email address by opening the link below: 11 | 12 | {{ confirmationUrl|raw }} 13 | 14 | After completing the confirmation, you can{% else %}You can now{% endif %} sign in with this email address and the password that you chose during sign up. 15 | 16 | If you have any questions, please feel free to ask us for help. 17 | 18 | {% include 'mail/en-US/includes/footer.txt' %} 19 | -------------------------------------------------------------------------------- /Website/views/mail/en-US/email-changed.txt: -------------------------------------------------------------------------------- 1 | {# 2 | * PHP-Foundation (https://github.com/delight-im/PHP-Foundation) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the MIT License (https://opensource.org/licenses/MIT) 5 | #} 6 | {% include 'mail/en-US/includes/header.txt' %} 7 | 8 | We wanted to let you know that your email address for {{ projectName|raw }} has recently been changed{% if oldEmailAddress %} from <{{ oldEmailAddress|raw }}>{% endif %}{% if newEmailAddress %} to <{{ newEmailAddress|raw }}>{% endif %}. 9 | 10 | If you didn’t make this change, please contact us at our email address that you find below. 11 | 12 | Feel free to ask us for help if you have any other questions as well. 13 | 14 | {% include 'mail/en-US/includes/footer.txt' %} 15 | -------------------------------------------------------------------------------- /Website/views/mail/de-DE/password-changed.txt: -------------------------------------------------------------------------------- 1 | {# 2 | * PHP-Foundation (https://github.com/delight-im/PHP-Foundation) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the MIT License (https://opensource.org/licenses/MIT) 5 | #} 6 | {% include 'mail/de-DE/includes/header.txt' %} 7 | 8 | wir möchten dich darüber informieren, dass dein Passwort für {{ projectName|raw }} kürzlich geändert wurde. 9 | 10 | Bitte verwende von nun an das neue Passwort, um dich in deinem Benutzerkonto anzumelden. 11 | 12 | Wenn du diese Änderung nicht vorgenommen hast, kontaktiere uns bitte über unsere E-Mail-Adresse, die du unten findest. 13 | 14 | Solltest du noch andere Fragen haben, kannst du uns ebenfalls gerne um Hilfe bitten. 15 | 16 | {% include 'mail/de-DE/includes/footer.txt' %} 17 | -------------------------------------------------------------------------------- /Website/views/privacy_policy.html: -------------------------------------------------------------------------------- 1 | {# 2 | * MovieContentFilter (https://www.moviecontentfilter.com/) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the GNU AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.txt) 5 | #} 6 | {% include 'includes/page_top.html' %} 7 | 11 |
12 |
13 |
14 | 17 |
18 | {{ htmlSource | raw }} 19 |
20 |
21 |
22 |
23 | {% include 'includes/page_bottom.html' %} 24 | -------------------------------------------------------------------------------- /Website/app/Lib/Annotation.php: -------------------------------------------------------------------------------- 1 | timing = $timing; 24 | } 25 | 26 | /** 27 | * Returns the time range of this instance 28 | * 29 | * @return Timing the time range 30 | */ 31 | public function getTiming() { 32 | return $this->timing; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Website/views/mail/de-DE/sign-up.txt: -------------------------------------------------------------------------------- 1 | {# 2 | * PHP-Foundation (https://github.com/delight-im/PHP-Foundation) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the MIT License (https://opensource.org/licenses/MIT) 5 | #} 6 | {% include 'mail/de-DE/includes/header.txt' %} 7 | 8 | willkommen bei {{ projectName|raw }}! 9 | 10 | {%if confirmationUrl %}Bitte bestätige deine E-Mail-Adresse, indem du den folgenden Link öffnest: 11 | 12 | {{ confirmationUrl|raw }} 13 | 14 | Nachdem du die Bestätigung abgeschlossen hast, kannst du dich{% else %}Du kannst dich jetzt{% endif %} mit dieser E-Mail-Adresse und dem Passwort, das du während der Registrierung gewählt hast, anmelden. 15 | 16 | Wenn du Fragen hast, kannst du uns gerne um Hilfe bitten. 17 | 18 | {% include 'mail/de-DE/includes/footer.txt' %} 19 | -------------------------------------------------------------------------------- /Website/views/404.html: -------------------------------------------------------------------------------- 1 | {# 2 | * MovieContentFilter (https://www.moviecontentfilter.com/) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the GNU AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.txt) 5 | #} 6 | {% include 'includes/page_top.html' %} 7 | 11 |
12 |
13 |
14 | 15 |

We’re sorry, the requested page could not be found.

16 |

Go back

17 |
18 |
19 |
20 | {% include 'includes/page_bottom.html' %} 21 | -------------------------------------------------------------------------------- /Website/views/mail/en-US/includes/footer.txt: -------------------------------------------------------------------------------- 1 | {# 2 | * PHP-Foundation (https://github.com/delight-im/PHP-Foundation) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the MIT License (https://opensource.org/licenses/MIT) 5 | #} 6 | Sincerely, 7 | {{ projectName|raw }} 8 | {{ projectUrl|raw }} 9 | 10 | -- 11 | 12 | {% if reasonForEmailDelivery %}{{ reasonForEmailDelivery|raw }} 13 | 14 | {% endif %}This message was sent to <{{ recipientEmailAddress|raw }}>{% if requestedByIpAddress %} and was initiated by someone on IP address “{{ requestedByIpAddress|raw }}”{% endif %}. 15 | 16 | Please do not reply to this message directly, as we are unable to respond from this email address. Instead, please contact us at <{{ projectEmail|raw }}>. 17 | 18 | © {{ projectName|raw }}, {{ projectPostalAddress|raw }} 19 | -------------------------------------------------------------------------------- /Website/views/mail/de-DE/email-changed.txt: -------------------------------------------------------------------------------- 1 | {# 2 | * PHP-Foundation (https://github.com/delight-im/PHP-Foundation) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the MIT License (https://opensource.org/licenses/MIT) 5 | #} 6 | {% include 'mail/de-DE/includes/header.txt' %} 7 | 8 | wir möchten dich darüber informieren, dass deine E-Mail-Adresse für {{ projectName|raw }} kürzlich{% if oldEmailAddress %} von <{{ oldEmailAddress|raw }}>{% endif %}{% if newEmailAddress %} zu <{{ newEmailAddress|raw }}>{% endif %} geändert wurde. 9 | 10 | Wenn du diese Änderung nicht vorgenommen hast, kontaktiere uns bitte über unsere E-Mail-Adresse, die du unten findest. 11 | 12 | Solltest du noch andere Fragen haben, kannst du uns ebenfalls gerne um Hilfe bitten. 13 | 14 | {% include 'mail/de-DE/includes/footer.txt' %} 15 | -------------------------------------------------------------------------------- /Website/views/mail/de-DE/includes/footer.txt: -------------------------------------------------------------------------------- 1 | {# 2 | * PHP-Foundation (https://github.com/delight-im/PHP-Foundation) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the MIT License (https://opensource.org/licenses/MIT) 5 | #} 6 | Mit freundlichen Grüßen 7 | {{ projectName|raw }} 8 | {{ projectUrl|raw }} 9 | 10 | -- 11 | 12 | {% if reasonForEmailDelivery %}{{ reasonForEmailDelivery|raw }} 13 | 14 | {% endif %}Diese Nachricht wurde an <{{ recipientEmailAddress|raw }}> gesendet{% if requestedByIpAddress %} und wurde durch jemanden mit der IP-Adresse „{{ requestedByIpAddress|raw }}“ verursacht{% endif %}. 15 | 16 | Bitte antworte nicht direkt auf diese Nachricht, da wir von dieser E-Mail-Adresse aus nicht antworten können. Kontaktiere uns bitte stattdessen unter <{{ projectEmail|raw }}>. 17 | 18 | © {{ projectName|raw }}, {{ projectPostalAddress|raw }} 19 | -------------------------------------------------------------------------------- /Website/app/Lib/Xml.php: -------------------------------------------------------------------------------- 1 | ', '>', $text); 24 | // quotation mark 25 | $text = \str_replace('"', '"', $text); 26 | // apostrophe 27 | $text = \str_replace('\'', ''', $text); 28 | 29 | return $text; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /Website/views/contact.html: -------------------------------------------------------------------------------- 1 | {# 2 | * MovieContentFilter (https://www.moviecontentfilter.com/) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the GNU AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.txt) 5 | #} 6 | {% include 'includes/page_top.html' %} 7 | 11 |
12 |
13 |
14 | 17 |
18 | 19 | {{ imageAlt }} 20 | 21 |
22 |
23 |
24 |
25 | {% include 'includes/page_bottom.html' %} 26 | -------------------------------------------------------------------------------- /Website/views/mcf/example.mcf: -------------------------------------------------------------------------------- 1 | {# 2 | * MovieContentFilter (https://www.moviecontentfilter.com/) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the GNU AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.txt) 5 | #} 6 | WEBVTT MovieContentFilter {{ version }} 7 | {% if version == '1.1.0' %} 8 | 9 | NOTE 10 | TITLE Ozymandias 11 | YEAR 2013 12 | TYPE episode 13 | SEASON 5 14 | EPISODE 14 15 | SOURCE https://www.moviecontentfilter.com/works/7Cmr6B 16 | IMDB http://www.imdb.com/title/tt2301451/ 17 | RELEASE North America 18 | COMMENT Quite an intense episode! 19 | {% endif %} 20 | 21 | NOTE 22 | START 00:00:04.020 23 | END 01:24:00.100 24 | 25 | 00:00:06.075 --> 00:00:10.500 26 | violence=high 27 | 28 | 00:06:14.000 --> 00:06:17.581 29 | gambling=medium # Some comment 30 | drugs=high=video 31 | 32 | 00:58:59.118 --> 01:00:03.240 33 | sex=low=both # Another comment 34 | 35 | 01:02:31.020 --> 01:02:49.800 36 | fear=low 37 | language=high=audio 38 | -------------------------------------------------------------------------------- /Website/views/help.html: -------------------------------------------------------------------------------- 1 | {# 2 | * MovieContentFilter (https://www.moviecontentfilter.com/) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the GNU AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.txt) 5 | #} 6 | {% include 'includes/page_top.html' %} 7 | 11 |
12 |
13 | 21 |
22 |
23 | {% include 'includes/page_bottom.html' %} 24 | -------------------------------------------------------------------------------- /Website/views/includes/new_password_2x.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |

6 | Please create a unique password just for this service. It must be at least {{ passwordMinLength }} characters long. Feel free to write the password down on paper or to use a password manager. You shouldn’t have to keep it in mind. 7 |

8 |
9 |
10 |
11 | 12 |
13 | 14 |

Please repeat your password here, just to prevent any typing errors.

15 |
16 |
17 | -------------------------------------------------------------------------------- /Website/views/reset_password.html: -------------------------------------------------------------------------------- 1 | {# 2 | * MovieContentFilter (https://www.moviecontentfilter.com/) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the GNU AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.txt) 5 | #} 6 | {% include 'includes/page_top.html' %} 7 | 11 |
12 |
13 |
14 | 23 |
24 | {% include 'includes/new_password_2x.html' with { 'isReset': true, 'passwordMinLength': passwordMinLength } only %} 25 |
26 |
27 | 28 |
29 |
30 |
31 |
32 |
33 |
34 | {% include 'includes/page_bottom.html' %} 35 | -------------------------------------------------------------------------------- /Website/views/includes/annotation_list_group_item_content.html: -------------------------------------------------------------------------------- 1 | {# 2 | * MovieContentFilter (https://www.moviecontentfilter.com/) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the GNU AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.txt) 5 | #} 6 |

7 | {% if annotation.category_is_general %} 8 | General 9 | {% else %} 10 | {{ annotation.category_label }} 11 | {% endif %} 12 | 13 | {{ annotation.channel_label }} 14 | 15 |

16 |
17 |
18 |
19 | 20 | Severity: {{ annotation.severity }} 21 | 22 | 23 | {{ annotation.start_time }} – {{ annotation.end_time }} 24 | 25 |
26 |
27 |
28 | -------------------------------------------------------------------------------- /Website/app/Lib/Playlist/FilePlaylist.php: -------------------------------------------------------------------------------- 1 | mediaFileUri = $mediaFileUri; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Website/views/preferences.html: -------------------------------------------------------------------------------- 1 | {# 2 | * MovieContentFilter (https://www.moviecontentfilter.com/) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the GNU AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.txt) 5 | #} 6 | {% include 'includes/page_top.html' %} 7 | 11 |
12 |
13 |
14 | 15 | 19 | 27 |
28 |
29 |
30 | {% include 'includes/page_bottom.html' %} 31 | -------------------------------------------------------------------------------- /Website/views/includes/page_bottom.html: -------------------------------------------------------------------------------- 1 | {# 2 | * MovieContentFilter (https://www.moviecontentfilter.com/) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the GNU AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.txt) 5 | #} 6 |
7 |
8 | 20 |
21 | {% include 'includes/scripts.html' %} 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Website/views/includes/page_header.html: -------------------------------------------------------------------------------- 1 | {# 2 | * MovieContentFilter (https://www.moviecontentfilter.com/) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the GNU AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.txt) 5 | #} 6 | 7 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | MovieContentFilter 21 | 22 | 23 | 24 | 28 | -------------------------------------------------------------------------------- /Website/app/Controller.php: -------------------------------------------------------------------------------- 1 | auth()->check()) { 18 | // fail with a proper response for non-authenticated users 19 | self::failNotSignedIn($app, $targetPath); 20 | } 21 | } 22 | 23 | public static function failNotSignedIn(App $app, $targetPath = null) { 24 | // if the requested target path has not been provided 25 | if (empty($targetPath)) { 26 | // use the current route as the default 27 | $targetPath = $app->currentRoute(); 28 | } 29 | 30 | // redirect back to the index and tell the user to sign in 31 | $app->flash()->warning('Please sign in to view the requested page. If you don’t have an account yet, you may sign up for free.'); 32 | $app->redirect('/?continue=' . \urlencode($targetPath)); 33 | exit; 34 | } 35 | 36 | public static function failNotFound(App $app) { 37 | // return the appropriate error code 38 | $app->setStatus(404); 39 | // return the view for the error page 40 | echo $app->view('404.html'); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /Website/app/AnnotationViewerTrait.php: -------------------------------------------------------------------------------- 1 | 8 |
  • Home
  • 9 |
  • Forgot password
  • 10 | 11 |
    12 |
    13 |
    14 | 15 |
    16 |
    17 | 18 |
    19 | 20 |

    Please enter the email address that you used to sign up. Make sure you can receive emails at this address, please. We will send information to you explaining how to reset your password.

    21 |
    22 |
    23 |
    24 |
    25 | 26 |
    27 |
    28 |
    29 |
    30 |
    31 |
    32 | {% include 'includes/page_bottom.html' %} 33 | -------------------------------------------------------------------------------- /Website/views/resend_confirmation.html: -------------------------------------------------------------------------------- 1 | {# 2 | * MovieContentFilter (https://www.moviecontentfilter.com/) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the GNU AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.txt) 5 | #} 6 | {% include 'includes/page_top.html' %} 7 | 11 |
    12 |
    13 |
    14 | 15 |
    16 |
    17 | 18 |
    19 | 20 |

    Please enter the email address that you used to sign up. Make sure you can receive emails at this address, please. We will try to send a new confirmation email to you.

    21 |
    22 |
    23 |
    24 |
    25 | 26 |
    27 |
    28 |
    29 |
    30 |
    31 |
    32 | {% include 'includes/page_bottom.html' %} 33 | -------------------------------------------------------------------------------- /Website/views/works_add_step_1.html: -------------------------------------------------------------------------------- 1 | {# 2 | * MovieContentFilter (https://www.moviecontentfilter.com/) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the GNU AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.txt) 5 | #} 6 | {% include 'includes/page_top.html' %} 7 | 11 |
    12 |
    13 |
    14 | 15 |
    16 |
    17 | 18 |
    19 |
    20 | 24 |
    25 |
    26 | 30 |
    31 |
    32 |
    33 |
    34 |
    35 | 36 |
    37 |
    38 |
    39 |
    40 |
    41 |
    42 | {% include 'includes/page_bottom.html' %} 43 | -------------------------------------------------------------------------------- /Website/app/Lib/Edl.php: -------------------------------------------------------------------------------- 1 | annotations as $annotation) { 29 | if ($annotation instanceof FilterableAnnotation xor $this->inverted) { 30 | $startStr = (string) $annotation->getTiming()->getStart(); 31 | $endStr = (string) $annotation->getTiming()->getEnd(); 32 | 33 | if ($annotation instanceof PlaylistItem) { 34 | if ($annotation->containsChannel('video') || $annotation->containsChannel('both')) { 35 | $action = self::ACTION_SKIP; 36 | } 37 | else { 38 | $action = self::ACTION_MUTE; 39 | } 40 | } 41 | else { 42 | $action = self::ACTION_SKIP; 43 | } 44 | 45 | $lines[] = $startStr . ' ' . $endStr . ' ' . $action; 46 | } 47 | } 48 | 49 | if (\count($lines) > 0) { 50 | $lines[] = ''; 51 | } 52 | 53 | return \implode("\n", $lines); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MovieContentFilter 2 | 3 | Watch movies with the freedom (not) to filter 4 | 5 | * [Go to website](https://www.moviecontentfilter.com/) 6 | 7 | ## How it works 8 | 9 | 1. The community tags the source material, i.e. movies and TV shows, with filterable categories. 10 | 1. You decide what you want to filter out and what you want to see. Customized filters are generated for you. 11 | 1. Having chosen from various categories and severity levels, you will enjoy a comfortable viewing and hearing experience. 12 | 13 | ## Commercial providers 14 | 15 | This project provides an open standard and shareable content under free licenses. 16 | 17 | There are some commercial offerings with similar goals. However, these services provide only proprietary filters and often they are not available worldwide. 18 | 19 | * [ClearPlay](https://www.clearplay.com/) 20 | * [VidAngel](https://www.vidangel.com/) 21 | * [enJoy Movie Filtering](http://www.enjoymoviesyourway.com/) 22 | * [TVGuardian](http://www.tvguardian.com/) 23 | 24 | ## Requirements 25 | 26 | * See [delight-im/PHP-Foundation](https://github.com/delight-im/PHP-Foundation#requirements) 27 | 28 | ## Installation 29 | 30 | * See [delight-im/PHP-Foundation](https://github.com/delight-im/PHP-Foundation#installation) 31 | 32 | ## Contributing 33 | 34 | All contributions are welcome! If you wish to contribute, please create an issue first so that your feature, problem or question can be discussed. 35 | 36 | ## License 37 | 38 | This project is licensed under the terms of the [GNU AGPL v3](https://www.gnu.org/licenses/agpl-3.0.txt). 39 | -------------------------------------------------------------------------------- /Website/views/browse_list.html: -------------------------------------------------------------------------------- 1 | {# 2 | * MovieContentFilter (https://www.moviecontentfilter.com/) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the GNU AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.txt) 5 | #} 6 | {% include 'includes/page_top.html' %} 7 | 11 |
    12 |
    13 |
    14 | 17 | 21 |
    22 | {% for work in works %} 23 | 24 | {{ work.title }} 25 | {{ work.year }} 26 | 27 | {% endfor %} 28 |
    29 |

    30 | Add a {{ typeSingular }} 31 |

    32 |
    33 |
    34 |
    35 | {% include 'includes/page_bottom.html' %} 36 | -------------------------------------------------------------------------------- /Website/views/browse_which.html: -------------------------------------------------------------------------------- 1 | {# 2 | * MovieContentFilter (https://www.moviecontentfilter.com/) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the GNU AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.txt) 5 | #} 6 | {% include 'includes/page_top.html' %} 7 | 11 |
    12 |
    13 | 14 |
    15 |

    Movies

    16 | {% if examples.movies %} 17 |

    18 | {% for movie in examples.movies %}{% if not loop.first %}, {% endif %}{{ movie.title }} ({{ movie.year }}){% endfor %} 19 | and more … 20 |

    21 | {% endif %} 22 |
    23 |
    24 |

    TV series

    25 | {% if examples.series %} 26 |

    27 | {% for movie in examples.series %}{% if not loop.first %}, {% endif %}{{ movie.title }} ({{ movie.year }}){% endfor %} 28 | and more … 29 |

    30 | {% endif %} 31 |
    32 |
    33 |

    or

    34 |

    35 | Add a movie or TV series 36 |

    37 |
    38 |
    39 |
    40 | {% include 'includes/page_bottom.html' %} 41 | -------------------------------------------------------------------------------- /Website/app/Lib/ContentualAnnotation.php: -------------------------------------------------------------------------------- 1 | category = (int) $category; 33 | $this->severity = (int) $severity; 34 | 35 | if ($channel !== null) { 36 | $this->channel = (int) $channel; 37 | } 38 | } 39 | 40 | /** 41 | * Returns the category ID of this entry 42 | * 43 | * @return int 44 | */ 45 | public function getCategory() { 46 | return $this->category; 47 | } 48 | 49 | /** 50 | * Returns the severity ID of this entry 51 | * 52 | * @return int 53 | */ 54 | public function getSeverity() { 55 | return $this->severity; 56 | } 57 | 58 | /** 59 | * Returns the channel ID of this entry 60 | * 61 | * @return int 62 | */ 63 | public function getChannel() { 64 | return $this->channel; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /Website/views/includes/modals/contribute/select_topic.html: -------------------------------------------------------------------------------- 1 | {# 2 | * MovieContentFilter (https://www.moviecontentfilter.com/) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the GNU AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.txt) 5 | #} 6 | 34 | -------------------------------------------------------------------------------- /Website/app/Lib/Mcf/WebvttTiming.php: -------------------------------------------------------------------------------- 1 | ((?:[0-9]{2,}?):(?:[0-9]{2}?):(?:[0-9]{2}?).(?:[0-9]{3}?))/'; 19 | /** The string to use as a delimiter in string representations */ 20 | const DELIMITER_STRING = ' --> '; 21 | 22 | /** 23 | * Converts this instance to a string representation 24 | * 25 | * @return string the string representation 26 | */ 27 | public function __toString() { 28 | return ((string) $this->getStart()) . self::DELIMITER_STRING . ((string) $this->getEnd()); 29 | } 30 | 31 | /** 32 | * Parses an instance from the specified string 33 | * 34 | * @param string $str the string to parse 35 | * @return static a new instance of this class 36 | * @throws InvalidWebvttTimingException 37 | */ 38 | public static function parse($str) { 39 | if (\preg_match(self::REGEX, $str, $parts)) { 40 | $start = WebvttTimestamp::parse($parts[1]); 41 | $end = WebvttTimestamp::parse($parts[2]); 42 | 43 | return new static($start, $end); 44 | } 45 | else { 46 | throw new InvalidWebvttTimingException(); 47 | } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /Website/maintenance.php: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 14 | 15 | 16 | 17 | 18 | Maintenance 19 | 48 | 49 | 50 |
    51 |
    52 |

    We’ll be back soon!

    53 |

    54 | We’re performing some maintenance at the moment and will be back shortly. 55 | Sorry for the inconvenience! 56 |

    57 |
    58 |

    Wir sind gleich zurück!

    59 |

    60 | Wir führen gerade Wartungsarbeiten durch und sind in Kürze wieder da. 61 | Bitte entschuldigen Sie die Unannehmlichkeiten! 62 |

    63 |
    64 |
    65 | 66 | 67 | -------------------------------------------------------------------------------- /Website/app/Lib/Playlist/PlaylistItem.php: -------------------------------------------------------------------------------- 1 | detailsUrl = $detailsUrl; 32 | $this->channels = []; 33 | } 34 | 35 | /** 36 | * Returns the URL where more information about this instance is available 37 | * 38 | * @return string the URL 39 | */ 40 | public function getDetailsUrl() { 41 | return $this->detailsUrl; 42 | } 43 | 44 | /** 45 | * Adds a channel that this instance affects 46 | * 47 | * @param string $channel the channel 48 | */ 49 | public function addChannel($channel) { 50 | $this->channels[$channel] = true; 51 | } 52 | 53 | /** 54 | * Returns whether this instance affects the specified channel 55 | * 56 | * @param string $channel the channel to check 57 | * @return bool whether the channel is affected 58 | */ 59 | public function containsChannel($channel) { 60 | return isset($this->channels[$channel]) && $this->channels[$channel]; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /Website/app/BrowsingController.php: -------------------------------------------------------------------------------- 1 | db()->select( 19 | $query, 20 | [ 21 | $app->auth()->getUserId(), 22 | 'movie' 23 | ] 24 | ); 25 | $series = $app->db()->select( 26 | $query, 27 | [ 28 | $app->auth()->getUserId(), 29 | 'series' 30 | ] 31 | ); 32 | 33 | echo $app->view('browse_which.html', [ 34 | 'examples' => [ 35 | 'movies' => $movies, 36 | 'series' => $series 37 | ] 38 | ]); 39 | } 40 | 41 | public static function showCategory(App $app, $type) { 42 | $type = $app->input()->value($type); 43 | 44 | if ($type === 'movies') { 45 | $typeSingular = 'movie'; 46 | } 47 | else { 48 | $typeSingular = $type; 49 | } 50 | 51 | $numWorks = $app->db()->selectValue( 52 | 'SELECT COUNT(*) FROM works WHERE (is_public = 1 OR author_user_id = ?) AND type = ?', 53 | [ 54 | $app->auth()->getUserId(), 55 | $typeSingular 56 | ] 57 | ); 58 | 59 | $works = $app->db()->select( 60 | 'SELECT id, title, year FROM works WHERE (is_public = 1 OR author_user_id = ?) AND type = ? ORDER BY year DESC, title ASC LIMIT 0, 50', 61 | [ 62 | $app->auth()->getUserId(), 63 | $typeSingular 64 | ] 65 | ); 66 | 67 | echo $app->view('browse_list.html', [ 68 | 'typeSingular' => $typeSingular, 69 | 'numWorks' => $numWorks, 70 | 'works' => $works 71 | ]); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /Website/views/includes/modals/contribute/select_severity.html: -------------------------------------------------------------------------------- 1 | {# 2 | * MovieContentFilter (https://www.moviecontentfilter.com/) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the GNU AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.txt) 5 | #} 6 | 39 | -------------------------------------------------------------------------------- /Website/views/includes/modals/contribute/select_channel.html: -------------------------------------------------------------------------------- 1 | {# 2 | * MovieContentFilter (https://www.moviecontentfilter.com/) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the GNU AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.txt) 5 | #} 6 | 40 | -------------------------------------------------------------------------------- /Website/views/includes/modals/contribute/select_category.html: -------------------------------------------------------------------------------- 1 | {# 2 | * MovieContentFilter (https://www.moviecontentfilter.com/) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the GNU AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.txt) 5 | #} 6 | 39 | -------------------------------------------------------------------------------- /Website/app/Lib/M3u.php: -------------------------------------------------------------------------------- 1 | publicInfoUrl; 27 | 28 | foreach ($this->annotations as $annotation) { 29 | if ($annotation instanceof FilterableAnnotation xor $this->inverted) { 30 | if ($annotation instanceof PlaylistItem) { 31 | $lines[] = ''; 32 | $lines[] = '# ' . $annotation->getDetailsUrl(); 33 | } 34 | } 35 | else { 36 | $lines[] = ''; 37 | 38 | if ($annotation instanceof PlaylistItem) { 39 | $lines[] = '# ' . $annotation->getDetailsUrl(); 40 | } 41 | 42 | // if the end of this annotation extends up to the very end of the file 43 | if ($annotation->getTiming()->getEnd()->equals($this->endTime)) { 44 | // let this annotation end at zero seconds to symbolize the very end of the file 45 | $endTimestamp = static::createTimestampFromSeconds(0); 46 | } 47 | // if this annotation ends somewhere in the middle of the file 48 | else { 49 | // do not modify this annotation 50 | $endTimestamp = $annotation->getTiming()->getEnd(); 51 | } 52 | 53 | $lines[] = '#EXTVLCOPT:start-time=' . ((string) $annotation->getTiming()->getStart()); 54 | $lines[] = '#EXTVLCOPT:stop-time=' . ((string) $endTimestamp); 55 | $lines[] = $this->mediaFileUri; 56 | } 57 | } 58 | 59 | $lines[] = ''; 60 | 61 | return \implode("\n", $lines); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /Website/app/Lib/Mcf/Version.php: -------------------------------------------------------------------------------- 1 | major = (int) $major; 35 | $this->minor = (int) $minor; 36 | $this->patch = (int) $patch; 37 | } 38 | 39 | /** 40 | * Converts this instance to a string representation 41 | * 42 | * @return string the string representation 43 | */ 44 | public function __toString() { 45 | return $this->major . '.' . $this->minor . '.' . $this->patch; 46 | } 47 | 48 | /** 49 | * Parses an instance from the specified string 50 | * 51 | * @param string $str the string to parse 52 | * @return static a new instance of this class 53 | * @throws InvalidVersionException 54 | */ 55 | public static function parse($str) { 56 | if (\preg_match(self::REGEX, $str, $parts)) { 57 | $major = (int) $parts[1]; 58 | $minor = (int) $parts[2]; 59 | $patch = (int) $parts[3]; 60 | 61 | return new static($major, $minor, $patch); 62 | } 63 | else { 64 | throw new InvalidVersionException(); 65 | } 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /Website/views/view_multiple.html: -------------------------------------------------------------------------------- 1 | {# 2 | * MovieContentFilter (https://www.moviecontentfilter.com/) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the GNU AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.txt) 5 | #} 6 | {% include 'includes/page_top.html' %} 7 | 12 |
    13 |
    14 |
    15 | 16 |
    17 |
    18 | Add episode 19 | {% if deletable %} 20 | Delete 21 | {% endif %} 22 | More information 23 |
    24 |
    25 | {% if episodes %} 26 | 37 | {% else %} 38 |

    No episodes available 😞

    39 |

    Be the first to add one above.

    40 | {% endif %} 41 |
    42 |
    43 |
    44 | {% include 'includes/page_bottom.html' %} 45 | -------------------------------------------------------------------------------- /Website/public/css/normalize.min.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v4.1.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | progress,sub,sup{vertical-align:baseline}button,hr,input{overflow:visible}[type=checkbox],[type=radio],legend{box-sizing:border-box;padding:0}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent;-webkit-text-decoration-skip:objects}a:active,a:hover{outline-width:0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}svg:not(:root){overflow:hidden}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}button,input,select,textarea{font:inherit;margin:0}optgroup{font-weight:700}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:ButtonText dotted 1px}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{color:inherit;display:table;max-width:100%;white-space:normal}textarea{overflow:auto}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-input-placeholder{color:inherit;opacity:.54}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit} 4 | -------------------------------------------------------------------------------- /Website/views/preferences_by_topic.html: -------------------------------------------------------------------------------- 1 | {# 2 | * MovieContentFilter (https://www.moviecontentfilter.com/) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the GNU AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.txt) 5 | #} 6 | {% include 'includes/page_top.html' %} 7 | 12 |
    13 |
    14 |
    15 | 16 | 20 |
    21 | {% for category in categories %} 22 |
    23 | 26 |
    27 | 32 |
    33 |
    34 | {% endfor %} 35 |
    36 |
    37 | 38 |
    39 |
    40 |
    41 |
    42 |
    43 |
    44 | {% include 'includes/page_bottom.html' %} 45 | -------------------------------------------------------------------------------- /Website/app/EmailSenderTrait.php: -------------------------------------------------------------------------------- 1 | view($template, $params); 38 | 39 | // create a new message object 40 | /** @var \Swift_Mime_Message $obj */ 41 | $obj = $app->mail()->createMessage(); 42 | 43 | // configure the message 44 | $obj->setSubject($subject); 45 | $obj->setFrom( 46 | [ $_ENV['COM_MOVIECONTENTFILTER_MAIL_FROM'] => $_ENV['COM_MOVIECONTENTFILTER_PROJECT_NAME'] ] 47 | ); 48 | if (empty($toName)) { 49 | $obj->setTo( 50 | [ $toAddress ] 51 | ); 52 | } 53 | else { 54 | $obj->setTo( 55 | [ $toAddress => $toName ] 56 | ); 57 | } 58 | $obj->setBody($body); 59 | 60 | // send the message and return whether this operation succeeded 61 | return $app->mail()->send($obj); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /Website/views/sign-up.html: -------------------------------------------------------------------------------- 1 | {# 2 | * MovieContentFilter (https://www.moviecontentfilter.com/) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the GNU AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.txt) 5 | #} 6 | {% include 'includes/page_top.html' %} 7 | 11 |
    12 |
    13 |
    14 | 23 |
    24 |
    25 | 26 |
    27 | 28 |

    Please enter your email address here. You will use this to sign in afterwards. Make sure you can receive emails at this address, please.

    29 |
    30 |
    31 | {% include 'includes/new_password_2x.html' with { 'isReset': false, 'passwordMinLength': passwordMinLength } only %} 32 |
    33 | 34 |
    35 | 36 |

    Do you want your real name or some nickname to be displayed publicly? This will be shown along with any content that you contribute. If you’re not sure, leave this field empty. You’ll be able to change it later on as well.

    37 |
    38 |
    39 |
    40 |
    41 | 42 |
    43 |
    44 |
    45 |
    46 |
    47 |
    48 | {% include 'includes/page_bottom.html' %} 49 | -------------------------------------------------------------------------------- /Website/views/topic_in_work.html: -------------------------------------------------------------------------------- 1 | {# 2 | * MovieContentFilter (https://www.moviecontentfilter.com/) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the GNU AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.txt) 5 | #} 6 | {% include 'includes/page_top.html' %} 7 | 25 |
    26 |
    27 |
    28 | 36 | {% if annotations %} 37 | 44 | {% else %} 45 |

    No annotations of “{{ topic.label }}” available 😞

    46 | {% if app.auth().isLoggedIn() and app.auth().getUserId() == work.authorUserId %} 47 |

    Be the first to mark occurrences in “{{ work.title }}”.

    48 | {% endif %} 49 | {% endif %} 50 |
    51 |
    52 |
    53 | {% include 'includes/page_bottom.html' %} 54 | -------------------------------------------------------------------------------- /Website/views/includes/modals/contribute/select_next_action.html: -------------------------------------------------------------------------------- 1 | {# 2 | * MovieContentFilter (https://www.moviecontentfilter.com/) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the GNU AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.txt) 5 | #} 6 | 54 | -------------------------------------------------------------------------------- /Website/config/.env.example: -------------------------------------------------------------------------------- 1 | ########## BEGIN FRAMEWORK CONFIGURATION (DON'T CHANGE THE STRUCTURE HERE -- PLEASE SET ALL REQUIRED VALUES) ########## 2 | 3 | # Whether the application should be in debug mode (`1`) or in production mode (`0`) 4 | APP_DEBUG=1 5 | # The public HTTP(S) URL pointing at the application's root directory 6 | APP_PUBLIC_URL=http://localhost/ 7 | # The character set or encoding to use throughout the application (usually 'UTF-8') 8 | APP_CHARSET=UTF-8 9 | # The default timezone for the application (see 'http://php.net/manual/de/timezones.php' for possible values) 10 | APP_DEFAULT_TIMEZONE=UTC 11 | 12 | # The name of the database driver (e.g. `mysql` or `pgsql`) 13 | DB_DRIVER=mysql 14 | DB_HOST=localhost 15 | DB_PORT=3306 16 | DB_NAME=movie_content_filter 17 | DB_USERNAME=root 18 | DB_PASSWORD=monkey 19 | DB_CHARSET=utf8mb4 20 | DB_PREFIX= 21 | 22 | # The transport mechanism that is used to send mails (either `smtp`, `sendmail` or `php`) 23 | MAIL_TRANSPORT=php 24 | MAIL_HOST=smtp.example.com 25 | MAIL_PORT=587 26 | MAIL_USERNAME=user@example.com 27 | MAIL_PASSWORD=monkey 28 | MAIL_TLS=0 29 | 30 | # The settings of the encoder/decoder that can be used to obfuscate IDs 31 | SECURITY_IDS_ALPHABET=GZwBHpfWybgQ5d_2mM-jh84K69tqYknx7LN3zvDrcSJVRPXsCFT 32 | SECURITY_IDS_PRIME=1125812041 33 | SECURITY_IDS_INVERSE=348986105 34 | SECURITY_IDS_RANDOM=998048641 35 | 36 | ########## END FRAMEWORK CONFIGURATION ########## 37 | 38 | ########## BEGIN APP CONFIGURATION (PLEASE PUT YOUR OWN SETTINGS HERE (IF ANY)) ########## 39 | 40 | COM_MOVIECONTENTFILTER_PROJECT_NAME="MovieContentFilter" 41 | COM_MOVIECONTENTFILTER_PROJECT_POSTAL="123 Main Street, Anytown, USA" 42 | COM_MOVIECONTENTFILTER_MAIL_FROM="no-reply@example.com" 43 | COM_MOVIECONTENTFILTER_MAIL_REPLY_TO="info@example.com" 44 | COM_MOVIECONTENTFILTER_DATA_PROTECTION_SUPERVISORY_AUTHORITY_NAME="National Data Protection Authority of Anyland" 45 | COM_MOVIECONTENTFILTER_DATA_PROTECTION_SUPERVISORY_AUTHORITY_URL="http://privacy.example.org/" 46 | COM_MOVIECONTENTFILTER_CONTACT_IMAGE_URL_FULL="http://www.example.com/images/contact/full.png" 47 | COM_MOVIECONTENTFILTER_CONTACT_IMAGE_URL_MINIMAL="http://www.example.com/images/contact/minimal.png" 48 | COM_MOVIECONTENTFILTER_CONTACT_IMAGE_ALT="Click to listen to an accessible audio version" 49 | COM_MOVIECONTENTFILTER_CONTACT_IMAGE_WIDTH_FULL=420 50 | COM_MOVIECONTENTFILTER_CONTACT_IMAGE_WIDTH_MINIMAL=360 51 | COM_MOVIECONTENTFILTER_CONTACT_AUDIO_URL_FULL="http://www.example.com/audio/contact/full.mp3" 52 | COM_MOVIECONTENTFILTER_CONTACT_AUDIO_URL_MINIMAL="http://www.example.com/audio/contact/minimal.mp3" 53 | 54 | ########## END APP CONFIGURATION ########## 55 | -------------------------------------------------------------------------------- /Website/public/js/html5shiv.min.js: -------------------------------------------------------------------------------- 1 | /*! HTML5 Shiv 3.7.3 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed */ 2 | 3 | !function(a,b){function c(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function d(){var a=t.elements;return"string"==typeof a?a.split(" "):a}function e(a,b){var c=t.elements;"string"!=typeof c&&(c=c.join(" ")),"string"!=typeof a&&(a=a.join(" ")),t.elements=c+" "+a,j(b)}function f(a){var b=s[a[q]];return b||(b={},r++,a[q]=r,s[r]=b),b}function g(a,c,d){if(c||(c=b),l)return c.createElement(a);d||(d=f(c));var e;return e=d.cache[a]?d.cache[a].cloneNode():p.test(a)?(d.cache[a]=d.createElem(a)).cloneNode():d.createElem(a),!e.canHaveChildren||o.test(a)||e.tagUrn?e:d.frag.appendChild(e)}function h(a,c){if(a||(a=b),l)return a.createDocumentFragment();c=c||f(a);for(var e=c.frag.cloneNode(),g=0,h=d(),i=h.length;i>g;g++)e.createElement(h[g]);return e}function i(a,b){b.cache||(b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag()),a.createElement=function(c){return t.shivMethods?g(c,a,b):b.createElem(c)},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+d().join().replace(/[\w\-:]+/g,function(a){return b.createElem(a),b.frag.createElement(a),'c("'+a+'")'})+");return n}")(t,b.frag)}function j(a){a||(a=b);var d=f(a);return!t.shivCSS||k||d.hasCSS||(d.hasCSS=!!c(a,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),l||i(a,d),a}var k,l,m="3.7.3",n=a.html5||{},o=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,p=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,q="_html5shiv",r=0,s={};!function(){try{var a=b.createElement("a");a.innerHTML="",k="hidden"in a,l=1==a.childNodes.length||function(){b.createElement("a");var a=b.createDocumentFragment();return"undefined"==typeof a.cloneNode||"undefined"==typeof a.createDocumentFragment||"undefined"==typeof a.createElement}()}catch(c){k=!0,l=!0}}();var t={elements:n.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:n.shivCSS!==!1,supportsUnknownElements:l,shivMethods:n.shivMethods!==!1,type:"default",shivDocument:j,createElement:g,createDocumentFragment:h,addElements:e};a.html5=t,j(b),"object"==typeof module&&module.exports&&(module.exports=t)}("undefined"!=typeof window?window:this,document); 4 | -------------------------------------------------------------------------------- /Website/app/Lib/Mcf/Annotation.php: -------------------------------------------------------------------------------- 1 | content = []; 31 | } 32 | 33 | /** 34 | * Returns all descriptions of content from this instance 35 | * 36 | * @return Content[] the list of descriptions 37 | */ 38 | public function getContent() { 39 | return $this->content; 40 | } 41 | 42 | /** 43 | * Adds a single description of content to this instance 44 | * 45 | * @param Content $content the content description 46 | */ 47 | public function addContent(Content $content) { 48 | $this->content[] = $content; 49 | } 50 | 51 | /** 52 | * Converts this instance to a string representation 53 | * 54 | * @return string the string representation 55 | */ 56 | public function __toString() { 57 | $out = ''; 58 | 59 | $out .= (string) $this->timing; 60 | $out .= "\n"; 61 | 62 | $numContent = \count($this->content); 63 | 64 | for ($i = 0; $i < $numContent; $i++) { 65 | $out .= (string) $this->content[$i]; 66 | $out .= "\n"; 67 | } 68 | 69 | return $out; 70 | } 71 | 72 | /** 73 | * Parses an instance from the specified string 74 | * 75 | * @param string $str the string to parse 76 | * @return static a new instance of this class 77 | * @throws InvalidAnnotationException 78 | */ 79 | public static function parse($str) { 80 | $components = \preg_split(self::NEWLINE_REGEX, $str); 81 | $numComponents = \count($components); 82 | 83 | if ($numComponents >= 2) { 84 | $timing = WebvttTiming::parse($components[0]); 85 | 86 | $out = new static($timing); 87 | 88 | for ($i = 1; $i < $numComponents; $i++) { 89 | $out->addContent(Content::parse($components[$i])); 90 | } 91 | 92 | return $out; 93 | } 94 | else { 95 | throw new InvalidAnnotationException(); 96 | } 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /Website/app/Lib/Timing.php: -------------------------------------------------------------------------------- 1 | compareTo($start) <= 0) { 30 | throw new EmptyTimingException(); 31 | } 32 | 33 | $this->start = $start; 34 | $this->end = $end; 35 | } 36 | 37 | /** 38 | * Returns the start of this time range 39 | * 40 | * @return Timestamp the start 41 | */ 42 | public function getStart() { 43 | return $this->start; 44 | } 45 | 46 | /** 47 | * Returns the end of this time range 48 | * 49 | * @return Timestamp the end 50 | */ 51 | public function getEnd() { 52 | return $this->end; 53 | } 54 | 55 | /** 56 | * Returns the duration of this time range 57 | * 58 | * @return float the duration in seconds 59 | */ 60 | public function getDuration() { 61 | return $this->end->toSeconds() - $this->start->toSeconds(); 62 | } 63 | 64 | /** 65 | * Adds the specified number of seconds to this instance 66 | * 67 | * @param float $seconds the seconds to add 68 | */ 69 | public function addSeconds($seconds) { 70 | $this->start->addSeconds($seconds); 71 | $this->end->addSeconds($seconds); 72 | } 73 | 74 | /** 75 | * Subtracts the specified number of seconds from this instance 76 | * 77 | * @param float $seconds the seconds to subtract 78 | */ 79 | public function subtractSeconds($seconds) { 80 | $this->start->subtractSeconds($seconds); 81 | $this->end->subtractSeconds($seconds); 82 | } 83 | 84 | /** 85 | * Multiplies this instance with the specified factor 86 | * 87 | * @param float $factor the factor to apply 88 | */ 89 | public function multiply($factor) { 90 | $this->start->multiply($factor); 91 | $this->end->multiply($factor); 92 | } 93 | 94 | /** 95 | * Divides this instance by the specified divisor 96 | * 97 | * @param float $divisor the divisor to apply 98 | */ 99 | public function divide($divisor) { 100 | $this->start->divide($divisor); 101 | $this->end->divide($divisor); 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /Website/app/Lib/Mcf/WebvttTimestamp.php: -------------------------------------------------------------------------------- 1 | seconds; 29 | 30 | // calculate the hours portion 31 | $hours = \floor($secondsFloat / 3600); 32 | $hours = \min($hours, 99); 33 | 34 | // consume the hours 35 | $secondsFloat = \fmod($secondsFloat, 3600); 36 | 37 | // calculate the minutes portion 38 | $minutes = \floor($secondsFloat / 60); 39 | 40 | // consume the minutes 41 | $secondsFloat = \fmod($secondsFloat, 60); 42 | 43 | // calculate the seconds portion 44 | $seconds = \floor($secondsFloat); 45 | 46 | // consume the seconds 47 | $secondsFloat = \fmod($secondsFloat, 1); 48 | 49 | // calculate the milliseconds portion 50 | $milliseconds = (int) \round($secondsFloat * 1000); 51 | 52 | // return the formatted composite string 53 | return self::pad($hours, 2) . ':' . self::pad($minutes, 2) . ':' . self::pad($seconds, 2) . '.' . self::pad($milliseconds, 3); 54 | } 55 | 56 | /** 57 | * Parses an instance from the specified string 58 | * 59 | * @param string $str the string to parse 60 | * @return static a new instance of this class 61 | * @throws InvalidWebvttTimestampException 62 | */ 63 | public static function parse($str) { 64 | if (\preg_match(self::REGEX, $str, $parts)) { 65 | $hour = (int) $parts[1]; 66 | $minute = (int) $parts[2]; 67 | $second = (int) $parts[3]; 68 | $millisecond = (int) $parts[4]; 69 | 70 | return self::fromComponents($hour, $minute, $second, $millisecond); 71 | } 72 | else { 73 | throw new InvalidWebvttTimestampException(); 74 | } 75 | } 76 | 77 | /** 78 | * Pads the specified number with zeros until it reaches the desired length 79 | * 80 | * @param int $number the number to pad 81 | * @param int $length the number of digits to pad to 82 | * @return string the padded number as a string 83 | */ 84 | private static function pad($number, $length) { 85 | return \str_pad($number, $length, self::PAD_CHAR, \STR_PAD_LEFT); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /Website/views/annotation.html: -------------------------------------------------------------------------------- 1 | {# 2 | * MovieContentFilter (https://www.moviecontentfilter.com/) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the GNU AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.txt) 5 | #} 6 | {% include 'includes/page_top.html' with { 'includeIconFont': true } %} 7 | 26 |
    27 |
    28 |
    29 | 38 |
    39 |
    40 | 43 | 46 | 49 |
    50 |
    51 |
      52 |
    • 53 | {% include 'includes/annotation_list_group_item_content.html' with { 'annotation': annotation } only %} 54 |
    • 55 |
    56 |
    57 |
    58 |
    59 | {% include 'includes/page_bottom.html' %} 60 | -------------------------------------------------------------------------------- /Website/views/welcome.html: -------------------------------------------------------------------------------- 1 | {# 2 | * MovieContentFilter (https://www.moviecontentfilter.com/) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the GNU AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.txt) 5 | #} 6 | {% include 'includes/page_top.html' %} 7 |
    8 |
    9 |

    MovieContentFilter

    10 |

    Watch movies with the freedom (not) to filter

    11 |

    12 | Browse collection › 13 | {% if not app.auth().check() %} 14 | Create your free account › 15 | {% endif %} 16 |

    17 |
    18 |
    19 |
    20 |
    21 |
    22 |

    Like a good dinner

    23 |

    Movies and TV shows are like a good dinner: Do you really want to miss out on the whole experience just because you don’t like the sprouts?

    24 |
    25 |
    26 |

    It’s about choice

    27 |

    This project is not about censorship or automated filtering. It’s about choice. You decide what is shown and what is not.

    28 |
    29 |
    30 |

    No derivatives

    31 |

    Filter video and audio without having to change the original source material. No derivative videos needed! Thus your filters are legal and safe.

    32 |
    33 |
    34 |
    35 |
    36 |

    Voluntary and simple

    37 |

    There are better solutions than complex rating systems forced upon you by government agencies and industry committees.

    38 |
    39 |
    40 |

    No hardware, no costs

    41 |

    Buying special media players and getting proprietary filters really shouldn’t be necessary. Everything is free, both as in freedom and in free beer.

    42 |
    43 |
    44 |

    For art lovers

    45 |

    Film makers should not censor anything. They should release the film in the best, most complete and most artistic way possible.

    46 |
    47 |
    48 |
    49 |
    50 |

    Make your favorite movies and TV shows family-friendly. Or adjust your viewing and hearing experiences to respect individual phobias and anxieties. Or prevent your children from having nightmares and be as protective of them as you wish. Or show movies and TV series to people you care about, except for that one scene you can’t accept.

    51 |

    Have full control over every single scene you watch and listen to. Enjoy!

    52 |
    53 |
    54 |
    55 | {% include 'includes/page_bottom.html' %} 56 | -------------------------------------------------------------------------------- /Website/app/index.php: -------------------------------------------------------------------------------- 1 | get('/', '\App\MetaController::welcome'); 11 | $app->get('/works/:id', '\App\WorkController::showWork'); 12 | $app->get('/browse', '\App\BrowsingController::showOverview'); 13 | $app->get('/browse/:type', '\App\BrowsingController::showCategory'); 14 | $app->get('/works/:id/download', '\App\FilterController::customizeDownload'); 15 | $app->post('/works/:id/download', '\App\FilterController::sendDownload'); 16 | $app->get('/works/:id/contribute', '\App\AnnotationController::launchEditor'); 17 | $app->post('/works/:id/contribute', '\App\AnnotationController::receiveFromEditor'); 18 | $app->get('/works/:id/delete', '\App\WorkController::getDelete'); 19 | $app->post('/works/:id/delete', '\App\WorkController::postDelete'); 20 | $app->get('/preferences', '\App\PrefsController::showOverview'); 21 | $app->get('/preferences/:topicId', '\App\PrefsController::showTopic'); 22 | $app->post('/preferences/:topicId', '\App\PrefsController::saveTopic'); 23 | $app->get('/sign-up', '\App\AuthController::showSignUp'); 24 | $app->post('/sign-up', '\App\AuthController::saveSignUp'); 25 | $app->get('/confirm/:selector/:token', '\App\AuthController::confirmEmail'); 26 | $app->get('/resend-confirmation', '\App\AuthController::getResendConfirmation'); 27 | $app->post('/resend-confirmation', '\App\AuthController::postResendConfirmation'); 28 | $app->get('/add', '\App\WorkController::prepareWork'); 29 | $app->post('/add', '\App\WorkController::saveWork'); 30 | $app->post('/login', '\App\AuthController::processLogin'); 31 | $app->get('/annotations/:id', '\App\AnnotationController::showAnnotation'); 32 | $app->post('/annotations/:id/vote/:direction', '\App\AnnotationController::voteForAnnotation'); 33 | $app->get('/works/:workId/topics/:topicId', '\App\WorkController::showTopicInWork'); 34 | $app->get('/logout', '\App\AuthController::logout'); 35 | $app->get('/settings', '\App\SettingsController::getSettings'); 36 | $app->post('/settings/change-password', '\App\SettingsController::postChangePassword'); 37 | $app->post('/settings/change-email', '\App\SettingsController::postChangeEmail'); 38 | $app->post('/settings/control-password-reset', '\App\SettingsController::postControlPasswordReset'); 39 | $app->post('/settings/sign-out-everywhere', '\App\SettingsController::postLogOutEverywhere'); 40 | $app->get('/forgot-password', '\App\AuthController::getForgotPassword'); 41 | $app->post('/forgot-password', '\App\AuthController::postForgotPassword'); 42 | $app->get('/reset/:selector/:token', '\App\AuthController::getResetPassword'); 43 | $app->post('/reset/:selector/:token', '\App\AuthController::postResetPassword'); 44 | $app->get('/specification', '\App\MetaController::showSpecification'); 45 | $app->get('/help', '\App\MetaController::getHelp'); 46 | $app->get('/privacy', '\App\MetaController::showPrivacyPolicy'); 47 | $app->get('/contact', '\App\MetaController::showContactInformation'); 48 | 49 | // otherwise fail with "not found" 50 | \App\Controller::failNotFound($app); 51 | -------------------------------------------------------------------------------- /Website/views/work_delete.html: -------------------------------------------------------------------------------- 1 | {# 2 | * MovieContentFilter (https://www.moviecontentfilter.com/) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the GNU AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.txt) 5 | #} 6 | {% include 'includes/page_top.html' %} 7 | 25 |
    26 |
    27 |
    28 | 33 |
    34 |
    35 | 36 |
    37 |
    38 | 42 |
    43 |
    44 | 48 |
    49 |
    50 |
    51 |
    52 |
    53 | 54 |
    55 |
    56 |
    57 |
    58 |
    59 |
    60 | {% include 'includes/page_bottom.html' %} 61 | -------------------------------------------------------------------------------- /Website/views/includes/page_top.html: -------------------------------------------------------------------------------- 1 | {# 2 | * MovieContentFilter (https://www.moviecontentfilter.com/) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the GNU AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.txt) 5 | #} 6 | {% include 'includes/page_header.html' %} 7 | {% if includeIconFont %} 8 | 9 | {% endif %} 10 | 11 | 12 | 13 | 67 | {% include 'includes/flash/bootstrap-v3.html' %} 68 | -------------------------------------------------------------------------------- /Website/app/Lib/Xspf.php: -------------------------------------------------------------------------------- 1 | '; 26 | $lines[] = ''; 27 | $lines[] = "\t".'MovieContentFilter'; 28 | $lines[] = "\t".'' . Xml::escape($this->publicInfoUrl) . ''; 29 | $lines[] = "\t".''; 30 | 31 | $counter = 0; 32 | 33 | foreach ($this->annotations as $annotation) { 34 | $lines[] = "\t\t".''; 35 | 36 | if ($annotation instanceof PlaylistItem) { 37 | $lines[] = "\t\t\t".'' . Xml::escape($annotation->getDetailsUrl()) . ''; 38 | } 39 | else { 40 | $lines[] = "\t\t\t".''; 41 | } 42 | 43 | // if the end of this annotation extends up to the very end of the file 44 | if ($annotation->getTiming()->getEnd()->equals($this->endTime)) { 45 | // let this annotation end at zero seconds to symbolize the very end of the file 46 | $endTimestamp = static::createTimestampFromSeconds(0); 47 | } 48 | // if this annotation ends somewhere in the middle of the file 49 | else { 50 | // do not modify this annotation 51 | $endTimestamp = $annotation->getTiming()->getEnd(); 52 | } 53 | 54 | $lines[] = "\t\t\t".'' . Xml::escape($this->mediaFileUri) . ''; 55 | $lines[] = "\t\t\t".''; 56 | $lines[] = "\t\t\t\t".'' . $counter . ''; 57 | $lines[] = "\t\t\t\t".'start-time=' . ((string) $annotation->getTiming()->getStart()) . ''; 58 | $lines[] = "\t\t\t\t".'stop-time=' . ((string) $endTimestamp) . ''; 59 | 60 | if ($annotation instanceof FilterableAnnotation xor $this->inverted) { 61 | if ($annotation instanceof PlaylistItem) { 62 | if ($annotation->containsChannel('video') || $annotation->containsChannel('both')) { 63 | $lines[] = "\t\t\t\t".'no-video'; 64 | } 65 | 66 | if ($annotation->containsChannel('audio') || $annotation->containsChannel('both')) { 67 | $lines[] = "\t\t\t\t".'no-audio'; 68 | } 69 | } 70 | else { 71 | $lines[] = "\t\t\t\t".'no-video'; 72 | $lines[] = "\t\t\t\t".'no-audio'; 73 | } 74 | } 75 | 76 | $lines[] = "\t\t\t".''; 77 | $lines[] = "\t\t".''; 78 | 79 | $counter++; 80 | } 81 | 82 | $lines[] = "\t".''; 83 | $lines[] = ''; 84 | $lines[] = ''; 85 | 86 | return \implode("\n", $lines); 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /Website/views/view_single.html: -------------------------------------------------------------------------------- 1 | {# 2 | * MovieContentFilter (https://www.moviecontentfilter.com/) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the GNU AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.txt) 5 | #} 6 | {% include 'includes/page_top.html' %} 7 | 24 |
    25 |
    26 |
    27 | 40 |
    41 |
    42 | {% if topics %} 43 | Download filter 44 | {% endif %} 45 | {% if app.auth().isLoggedIn() and app.auth().getUserId() == authorUserId %} 46 | Contribute 47 | {% endif %} 48 | {% if deletable %} 49 | Delete 50 | {% endif %} 51 | More information 52 |
    53 |
    54 | {% if topics %} 55 | 79 | {% else %} 80 |

    No annotations of filterable content available 😞

    81 |

    Be the first to contribute above.

    82 | {% endif %} 83 |
    84 |
    85 |
    86 | {% include 'includes/page_bottom.html' %} 87 | -------------------------------------------------------------------------------- /Website/index.php: -------------------------------------------------------------------------------- 1 | overload(); 28 | } 29 | 30 | // in debug mode 31 | if (!isset($_ENV['APP_DEBUG']) || $_ENV['APP_DEBUG'] === '1') { 32 | // enable assertions 33 | \ini_set('assert.active', 1); 34 | @\ini_set('zend.assertions', 1); 35 | \ini_set('assert.exception', 1); 36 | 37 | // show errors 38 | \ini_set('display_errors', 1); 39 | \ini_set('display_startup_errors', 1); 40 | 41 | // terminate on errors and warnings 42 | (new \Whoops\Run())->pushHandler(new \Whoops\Handler\PlainTextHandler())->register(); 43 | } 44 | // in production mode 45 | else { 46 | // disable assertions 47 | \ini_set('assert.active', 0); 48 | @\ini_set('zend.assertions', -1); 49 | \ini_set('assert.exception', 0); 50 | 51 | // hide errors 52 | \ini_set('display_errors', 0); 53 | \ini_set('display_startup_errors', 0); 54 | } 55 | 56 | if (!isset($_ENV['APP_DEBUG']) && !isset($_ENV['APP_PUBLIC_URL'])) { 57 | throw new \RuntimeException('Environment variables not set'); 58 | } 59 | 60 | // if the internal character encoding has been configured 61 | if (isset($_ENV['APP_CHARSET'])) { 62 | // set the internal charset for PHP 63 | \mb_internal_encoding($_ENV['APP_CHARSET']); 64 | } 65 | 66 | // if the default timezone has been configured 67 | if (isset($_ENV['APP_DEFAULT_TIMEZONE'])) { 68 | // set the timezone for PHP 69 | \date_default_timezone_set($_ENV['APP_DEFAULT_TIMEZONE']); 70 | } 71 | 72 | // define a shorthand for access to the string handling methods 73 | function s($str) { 74 | return new \Delight\Str\Str($str); 75 | } 76 | 77 | // define a shorthand for access to HTML escaping 78 | function e($str) { 79 | return s($str)->escapeForHtml(); 80 | } 81 | 82 | // create the main application instance 83 | $app = new \Delight\Foundation\App(__DIR__ . '/storage/app', __DIR__ . '/views', __DIR__ . '/storage/framework'); 84 | 85 | // set the default content type and the correct charset for HTTP responses 86 | $app->setContentType('html'); 87 | 88 | // reluctantly put some constants into the global namespace for maximum convenience 89 | \define('TYPE_STRING', \Delight\Foundation\Input::DATA_TYPE_STRING); 90 | \define('TYPE_INT', \Delight\Foundation\Input::DATA_TYPE_INT); 91 | \define('TYPE_BOOL', \Delight\Foundation\Input::DATA_TYPE_BOOL); 92 | \define('TYPE_FLOAT', \Delight\Foundation\Input::DATA_TYPE_FLOAT); 93 | \define('TYPE_EMAIL', \Delight\Foundation\Input::DATA_TYPE_EMAIL); 94 | \define('TYPE_URL', \Delight\Foundation\Input::DATA_TYPE_URL); 95 | \define('TYPE_IP', \Delight\Foundation\Input::DATA_TYPE_IP); 96 | \define('TYPE_TEXT', \Delight\Foundation\Input::DATA_TYPE_TEXT); 97 | \define('TYPE_RAW', \Delight\Foundation\Input::DATA_TYPE_RAW); 98 | 99 | // include the actual application code 100 | require __DIR__ . '/app/index.php'; 101 | -------------------------------------------------------------------------------- /Website/app/Lib/Playlist/Playlist.php: -------------------------------------------------------------------------------- 1 | publicInfoUrl = $publicInfoUrl; 34 | $this->inverted = false; 35 | } 36 | 37 | /** 38 | * Sets whether this instance shall be inverted to keep only what would otherwise be filtered 39 | * 40 | * @param bool $inverted 41 | */ 42 | public function setInverted($inverted) { 43 | $this->inverted = (bool) $inverted; 44 | } 45 | 46 | /** 47 | * Returns whether this instance is inverted to keep only what would otherwise be filtered 48 | * 49 | * @return bool 50 | */ 51 | public function isInverted() { 52 | return $this->inverted; 53 | } 54 | 55 | /** Fills all gaps between individual annotations so that a continuous description is available */ 56 | public function fillUp() { 57 | $count = \count($this->annotations); 58 | 59 | $newAnnotations = []; 60 | 61 | $previousEndTime = static::createTimestampFromSeconds(0); 62 | 63 | for ($i = 0; $i < $count; $i++) { 64 | // get the start time of the current annotation 65 | $currentStart = $this->annotations[$i]->getTiming()->getStart(); 66 | // if there is a gap between this and the previous annotation 67 | if ($currentStart->compareTo($previousEndTime) > 0) { 68 | // add an unfilterable annotation in between to fill the gap 69 | $newAnnotations[] = new UnfilterableAnnotation( 70 | new Timing( 71 | $previousEndTime, 72 | $currentStart 73 | ) 74 | ); 75 | } 76 | 77 | // add the current annotation 78 | $newAnnotations[] = $this->annotations[$i]; 79 | 80 | // update the previous end time 81 | $previousEndTime = $this->annotations[$i]->getTiming()->getEnd(); 82 | } 83 | 84 | $fileEndTime = static::createTimestampFromSeconds($this->endTime->toSeconds()); 85 | 86 | // if there is a gap at the end of the file 87 | if ($previousEndTime->compareTo($fileEndTime) < 0) { 88 | // add an unfilterable annotation spanning from the end of the last annotation to the end of the file 89 | $newAnnotations[] = new UnfilterableAnnotation( 90 | new Timing( 91 | $previousEndTime, 92 | $fileEndTime 93 | ) 94 | ); 95 | } 96 | 97 | // replace the old annotations with the new 98 | $this->annotations = $newAnnotations; 99 | } 100 | 101 | /** 102 | * Creates a new timestamp from the specified time in seconds 103 | * 104 | * @param float $secondsFloat the time in seconds 105 | * @return Timestamp the new instance 106 | */ 107 | protected static function createTimestampFromSeconds($secondsFloat) { 108 | return Timestamp::fromSeconds($secondsFloat); 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /Website/app/Lib/Mcf/Content.php: -------------------------------------------------------------------------------- 1 | category = $category; 44 | $this->severity = $severity; 45 | $this->channel = $channel; 46 | $this->comment = $comment; 47 | } 48 | 49 | /** 50 | * Returns the category name of this entry as per MCF specification 51 | * 52 | * @return string 53 | */ 54 | public function getCategory() { 55 | return $this->category; 56 | } 57 | 58 | /** 59 | * Returns the severity level of this entry as per MCF specification 60 | * 61 | * @return string 62 | */ 63 | public function getSeverity() { 64 | return $this->severity; 65 | } 66 | 67 | /** 68 | * Returns the channel name of this entry as per MCF specification 69 | * 70 | * @return string 71 | */ 72 | public function getChannel() { 73 | return $this->channel; 74 | } 75 | 76 | /** 77 | * Returns the optional comment of this entry as per MCF specification 78 | * 79 | * @return string|null the comment or `null` 80 | */ 81 | public function getComment() { 82 | return $this->comment; 83 | } 84 | 85 | /** 86 | * Converts this instance to a string representation 87 | * 88 | * @return string the string representation 89 | */ 90 | public function __toString() { 91 | $out = $this->category . self::DELIMITER_CHAR . $this->severity . self::DELIMITER_CHAR . $this->channel; 92 | 93 | if ($this->comment !== null) { 94 | $out .= self::COMMENT_SEPARATOR . $this->comment; 95 | } 96 | 97 | return $out; 98 | } 99 | 100 | /** 101 | * Parses an instance from the specified string 102 | * 103 | * @param string $str the string to parse 104 | * @return static a new instance of this class 105 | * @throws InvalidContentException 106 | */ 107 | public static function parse($str) { 108 | if (\preg_match(self::REGEX, \trim($str), $parts)) { 109 | $channel = isset($parts[3]) ? $parts[3] : self::CHANNEL_DEFAULT; 110 | $comment = isset($parts[4]) ? $parts[4] : null; 111 | 112 | return new static($parts[1], $parts[2], $channel, $comment); 113 | } 114 | else { 115 | throw new InvalidContentException(); 116 | } 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /Website/.htaccess: -------------------------------------------------------------------------------- 1 | ### PHP-Foundation (https://github.com/delight-im/PHP-Foundation) 2 | ### Copyright (c) delight.im (https://www.delight.im/) 3 | ### Licensed under the MIT License (https://opensource.org/licenses/MIT) 4 | 5 | ########## BEGIN MAINTENANCE MODE ########## 6 | 7 | 8 | 9 | RewriteEngine On 10 | 11 | # Enable maintenance mode (Uncomment 1 line below) 12 | # RewriteRule . maintenance.php [END] 13 | 14 | 15 | 16 | ########## END MAINTENANCE MODE ########## 17 | 18 | ########## BEGIN PERFORMANCE AND SECURITY (https://github.com/delight-im/htaccess) ########## 19 | 20 | 21 | 22 | # Prevent clickjacking (forbids framing by third-party sites) 23 | Header set X-Frame-Options sameorigin 24 | 25 | # Prevent content sniffing (MIME sniffing) 26 | Header set X-Content-Type-Options nosniff 27 | 28 | # Attempt to enable XSS filters in browsers, if available, and block reflected XSS 29 | Header set X-XSS-Protection "1; mode=block" 30 | 31 | # Cache media files for a month 32 | 33 | Header set Cache-Control max-age=2629800 34 | 35 | 36 | # Remove response headers that provide no value but leak information 37 | Header unset X-Powered-By 38 | 39 | # Disable "ETag" headers so that browsers rely on the "Cache-Control" and "Expires" headers 40 | Header unset ETag 41 | 42 | 43 | 44 | 45 | 46 | # Turn off directory listings for folders without default documents 47 | Options -Indexes 48 | 49 | 50 | 51 | 52 | 53 | # Disable 'MultiViews' implicit filename pattern matches 54 | Options -MultiViews 55 | 56 | 57 | 58 | # Serve "text/plain" and "text/html" documents as UTF-8 by default 59 | AddDefaultCharset utf-8 60 | 61 | # Disable "ETag" headers so that browsers rely on the "Cache-Control" and "Expires" headers 62 | FileETag None 63 | 64 | ########## END PERFORMANCE AND SECURITY ########## 65 | 66 | ########## BEGIN CUSTOM (YOUR RULES GO HERE) ########## 67 | 68 | 69 | 70 | # Enable HTTP Strict Transport Security (HSTS) with a duration of six months (Uncomment 1 line below) 71 | # Header set Strict-Transport-Security max-age=15778800 72 | 73 | 74 | 75 | 76 | 77 | RewriteEngine On 78 | 79 | # Force 'www' (i.e. prefix the "bare" domain and all subdomains with 'www' through permanent redirects) (Uncomment 4 lines below) 80 | # RewriteCond %{HTTP_HOST} !^$ 81 | # RewriteCond %{HTTP_HOST} !^www\. [NC] 82 | # RewriteCond %{HTTPS}s ^on(s)| 83 | # RewriteRule ^ http%1://www.%{HTTP_HOST}%{REQUEST_URI} [R=301,L] 84 | 85 | # Force HTTPS (Uncomment 2 lines below) 86 | # RewriteCond %{HTTPS} off 87 | # RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L] 88 | 89 | 90 | 91 | # Prevent access to non-minified CSS and JS (Uncomment 3 lines below) 92 | # 93 | # Require all denied 94 | # 95 | 96 | # Announce contact information for security issues (Uncomment 2 lines below) 97 | Header set X-Vulnerability-Disclosure "https://www.delight.im/go/security" 98 | Header set X-Security-Contact "security@delight.im" 99 | 100 | ########## END CUSTOM ########## 101 | 102 | ########## BEGIN ROUTING (https://github.com/delight-im/PHP-Router) ########## 103 | 104 | 105 | 106 | RewriteEngine On 107 | 108 | # Don't rewrite requests for files in the 'public' directory 109 | RewriteRule ^(public)($|/) - [L] 110 | 111 | # For all other files first check if they exist in the 'public' directory 112 | RewriteCond %{DOCUMENT_ROOT}/public%{REQUEST_URI} -f 113 | RewriteRule ^ public%{REQUEST_URI} [L] 114 | 115 | # And let 'index.php' handle everything else 116 | RewriteRule . index.php [L] 117 | 118 | 119 | 120 | ########## END ROUTING ########## 121 | -------------------------------------------------------------------------------- /Website/public/js/respond.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Respond.js v1.4.2: min/max-width media query polyfill 3 | * Copyright 2013 Scott Jehl 4 | * Licensed under https://github.com/scottjehl/Respond/blob/master/LICENSE-MIT 5 | */ 6 | 7 | !function(a){"use strict";a.matchMedia=a.matchMedia||function(a){var b,c=a.documentElement,d=c.firstElementChild||c.firstChild,e=a.createElement("body"),f=a.createElement("div");return f.id="mq-test-1",f.style.cssText="position:absolute;top:-100em",e.style.background="none",e.appendChild(f),function(a){return f.innerHTML='­',c.insertBefore(e,d),b=42===f.offsetWidth,c.removeChild(e),{matches:b,media:a}}}(a.document)}(this),function(a){"use strict";function b(){u(!0)}var c={};a.respond=c,c.update=function(){};var d=[],e=function(){var b=!1;try{b=new a.XMLHttpRequest}catch(c){b=new a.ActiveXObject("Microsoft.XMLHTTP")}return function(){return b}}(),f=function(a,b){var c=e();c&&(c.open("GET",a,!0),c.onreadystatechange=function(){4!==c.readyState||200!==c.status&&304!==c.status||b(c.responseText)},4!==c.readyState&&c.send(null))};if(c.ajax=f,c.queue=d,c.regex={media:/@media[^\{]+\{([^\{\}]*\{[^\}\{]*\})+/gi,keyframes:/@(?:\-(?:o|moz|webkit)\-)?keyframes[^\{]+\{(?:[^\{\}]*\{[^\}\{]*\})+[^\}]*\}/gi,urls:/(url\()['"]?([^\/\)'"][^:\)'"]+)['"]?(\))/g,findStyles:/@media *([^\{]+)\{([\S\s]+?)$/,only:/(only\s+)?([a-zA-Z]+)\s?/,minw:/\([\s]*min\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/,maxw:/\([\s]*max\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/},c.mediaQueriesSupported=a.matchMedia&&null!==a.matchMedia("only all")&&a.matchMedia("only all").matches,!c.mediaQueriesSupported){var g,h,i,j=a.document,k=j.documentElement,l=[],m=[],n=[],o={},p=30,q=j.getElementsByTagName("head")[0]||k,r=j.getElementsByTagName("base")[0],s=q.getElementsByTagName("link"),t=function(){var a,b=j.createElement("div"),c=j.body,d=k.style.fontSize,e=c&&c.style.fontSize,f=!1;return b.style.cssText="position:absolute;font-size:1em;width:1em",c||(c=f=j.createElement("body"),c.style.background="none"),k.style.fontSize="100%",c.style.fontSize="100%",c.appendChild(b),f&&k.insertBefore(c,k.firstChild),a=b.offsetWidth,f?k.removeChild(c):c.removeChild(b),k.style.fontSize=d,e&&(c.style.fontSize=e),a=i=parseFloat(a)},u=function(b){var c="clientWidth",d=k[c],e="CSS1Compat"===j.compatMode&&d||j.body[c]||d,f={},o=s[s.length-1],r=(new Date).getTime();if(b&&g&&p>r-g)return a.clearTimeout(h),h=a.setTimeout(u,p),void 0;g=r;for(var v in l)if(l.hasOwnProperty(v)){var w=l[v],x=w.minw,y=w.maxw,z=null===x,A=null===y,B="em";x&&(x=parseFloat(x)*(x.indexOf(B)>-1?i||t():1)),y&&(y=parseFloat(y)*(y.indexOf(B)>-1?i||t():1)),w.hasquery&&(z&&A||!(z||e>=x)||!(A||y>=e))||(f[w.media]||(f[w.media]=[]),f[w.media].push(m[w.rules]))}for(var C in n)n.hasOwnProperty(C)&&n[C]&&n[C].parentNode===q&&q.removeChild(n[C]);n.length=0;for(var D in f)if(f.hasOwnProperty(D)){var E=j.createElement("style"),F=f[D].join("\n");E.type="text/css",E.media=D,q.insertBefore(E,o.nextSibling),E.styleSheet?E.styleSheet.cssText=F:E.appendChild(j.createTextNode(F)),n.push(E)}},v=function(a,b,d){var e=a.replace(c.regex.keyframes,"").match(c.regex.media),f=e&&e.length||0;b=b.substring(0,b.lastIndexOf("/"));var g=function(a){return a.replace(c.regex.urls,"$1"+b+"$2$3")},h=!f&&d;b.length&&(b+="/"),h&&(f=1);for(var i=0;f>i;i++){var j,k,n,o;h?(j=d,m.push(g(a))):(j=e[i].match(c.regex.findStyles)&&RegExp.$1,m.push(RegExp.$2&&g(RegExp.$2))),n=j.split(","),o=n.length;for(var p=0;o>p;p++)k=n[p],l.push({media:k.split("(")[0].match(c.regex.only)&&RegExp.$2||"all",rules:m.length-1,hasquery:k.indexOf("(")>-1,minw:k.match(c.regex.minw)&&parseFloat(RegExp.$1)+(RegExp.$2||""),maxw:k.match(c.regex.maxw)&&parseFloat(RegExp.$1)+(RegExp.$2||"")})}u()},w=function(){if(d.length){var b=d.shift();f(b.href,function(c){v(c,b.href,b.media),o[b.href]=!0,a.setTimeout(function(){w()},0)})}},x=function(){for(var b=0;bdb()->selectValue('SELECT COUNT(*) FROM categories'); 19 | $numTopics = $app->db()->selectValue('SELECT COUNT(*) FROM topics'); 20 | 21 | $topics = $app->db()->select( 22 | 'SELECT id, label, (SELECT COUNT(*) FROM preferences WHERE user_id = ? AND category_id IN (SELECT id FROM categories WHERE topic_id = topics.id)) AS categories_enabled FROM topics ORDER BY label ASC', 23 | [ $app->auth()->id() ] 24 | ); 25 | 26 | echo $app->view('preferences.html', [ 27 | 'numCategories' => $numCategories, 28 | 'numTopics' => $numTopics, 29 | 'topics' => $topics 30 | ]); 31 | } 32 | 33 | public static function showTopic(App $app, $topicId) { 34 | self::ensureAuthenticated($app); 35 | 36 | $topicId = $app->ids()->decode(\trim($topicId)); 37 | 38 | $topicName = $app->db()->selectValue( 39 | 'SELECT label FROM topics WHERE id = ?', 40 | [ $topicId ] 41 | ); 42 | 43 | $categories = $app->db()->select( 44 | 'SELECT id, label, is_general FROM categories AS a WHERE a.topic_id = ? ORDER BY a.is_general DESC, a.label ASC', 45 | [ $topicId ] 46 | ); 47 | 48 | $severities = $app->db()->select( 49 | 'SELECT id, name, label_in_preferences FROM severities WHERE available_as_preference = 1 ORDER BY inclusiveness ASC' 50 | ); 51 | 52 | $prefs = $app->db()->select( 53 | 'SELECT category_id, severity_id FROM preferences WHERE user_id = ?', 54 | [ $app->auth()->id() ] 55 | ); 56 | 57 | $initialValues = []; 58 | 59 | if ($prefs !== null) { 60 | foreach ($prefs as $pref) { 61 | // set the initial value for the preference 62 | $initialValues[$pref['category_id']] = $pref['severity_id']; 63 | } 64 | } 65 | 66 | echo $app->view('preferences_by_topic.html', [ 67 | 'topicName' => $topicName, 68 | 'categories' => $categories, 69 | 'severities' => $severities, 70 | 'initialValues' => $initialValues 71 | ]); 72 | } 73 | 74 | public static function saveTopic(App $app, $topicId) { 75 | self::ensureAuthenticated($app); 76 | 77 | $topicId = $app->ids()->decode(\trim($topicId)); 78 | 79 | if (isset($_POST['category']) && \is_array($_POST['category'])) { 80 | $validSeverityIds = $app->db()->selectColumn( 81 | 'SELECT id FROM severities WHERE available_as_annotation = 1' 82 | ); 83 | 84 | $categoryIdsToRemove = []; 85 | $categoriesToUpdate = []; 86 | 87 | foreach ($_POST['category'] as $categoryId => $severityId) { 88 | $categoryId = (int) $categoryId; 89 | $severityId = (int) $severityId; 90 | 91 | if (\in_array($severityId, $validSeverityIds)) { 92 | $categoriesToUpdate[$categoryId] = $severityId; 93 | } 94 | else { 95 | $categoryIdsToRemove[] = $categoryId; 96 | } 97 | } 98 | 99 | if (\count($categoryIdsToRemove) > 0) { 100 | $app->db()->exec( 101 | 'DELETE FROM preferences WHERE user_id = ? AND category_id IN (' . \implode(',', $categoryIdsToRemove) . ')', 102 | [ $app->auth()->id() ] 103 | ); 104 | } 105 | 106 | if (\count($categoriesToUpdate) > 0) { 107 | $categoryUpdateValues = []; 108 | 109 | foreach ($categoriesToUpdate as $categoryId => $severityId) { 110 | $categoryUpdateValues[] = '(' . $app->auth()->id() . ', ' . $categoryId . ', ' . $severityId . ')'; 111 | } 112 | 113 | $app->db()->exec( 114 | 'INSERT INTO preferences (user_id, category_id, severity_id) VALUES ' . \implode(',', $categoryUpdateValues) . ' ON DUPLICATE KEY UPDATE severity_id = VALUES(severity_id)' 115 | ); 116 | } 117 | 118 | // redirect back to the preferences overview 119 | $app->flash()->success('Your preferences have been saved!'); 120 | $app->redirect('/preferences'); 121 | exit; 122 | } 123 | 124 | // redirect back to the page where these preferences can be viewed (and edited) 125 | $app->flash()->warning('Your preferences could not be saved. Please try again!'); 126 | $app->redirect('/preferences/' . $app->ids()->encode($topicId)); 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /Website/app/Lib/Timestamp.php: -------------------------------------------------------------------------------- 1 | seconds = (float) $seconds; 29 | } 30 | 31 | /** 32 | * Adds the specified number of seconds to this instance 33 | * 34 | * @param float $seconds the seconds to add 35 | */ 36 | public function addSeconds($seconds) { 37 | $this->seconds += (float) $seconds; 38 | } 39 | 40 | /** 41 | * Subtracts the specified number of seconds from this instance 42 | * 43 | * @param float $seconds the seconds to subtract 44 | */ 45 | public function subtractSeconds($seconds) { 46 | $this->seconds -= (float) $seconds; 47 | } 48 | 49 | /** 50 | * Multiplies this instance with the specified factor 51 | * 52 | * @param float $factor the factor to apply 53 | */ 54 | public function multiply($factor) { 55 | $this->seconds *= (float) $factor; 56 | } 57 | 58 | /** 59 | * Divides this instance by the specified divisor 60 | * 61 | * @param float $divisor the divisor to apply 62 | */ 63 | public function divide($divisor) { 64 | $this->seconds /= (float) $divisor; 65 | } 66 | 67 | /** 68 | * Converts the time to seconds 69 | * 70 | * @return float the time in seconds 71 | */ 72 | public function toSeconds() { 73 | return $this->seconds; 74 | } 75 | 76 | /** 77 | * Compares this instance against the other specified instance 78 | * 79 | * @param Timestamp $other another instance of this class to compare with 80 | * @return int whether this instance is less than (result < 0), equal to (result = 0) or greater than (result > 0) 81 | */ 82 | public function compareTo(Timestamp $other) { 83 | $delta = $this->seconds - $other->seconds; 84 | 85 | if (\abs($delta) <= 1) { 86 | $delta *= self::NANOSECONDS_PER_SECOND; 87 | } 88 | 89 | return (int) $delta; 90 | } 91 | 92 | /** 93 | * Returns whether this instance is equal to the other specified instance 94 | * 95 | * @param Timestamp $other another instance of this class to compare with 96 | * @return bool 97 | */ 98 | public function equals(Timestamp $other) { 99 | return $this->compareTo($other) === 0; 100 | } 101 | 102 | /** 103 | * Creates a new instance from the specified time in seconds 104 | * 105 | * @param float $secondsFloat the time in seconds 106 | * @return static a new instance of this class 107 | */ 108 | public static function fromSeconds($secondsFloat) { 109 | return new static($secondsFloat); 110 | } 111 | 112 | /** 113 | * Creates a new instance from the specified individual components 114 | * 115 | * @param int $hour the hour (0-99) 116 | * @param int $minute the minute (0-59) 117 | * @param int $second the second (0-59) 118 | * @param int $millisecond the millisecond (0-999) 119 | * @return static a new instance of this class 120 | */ 121 | public static function fromComponents($hour, $minute, $second, $millisecond) { 122 | $secondsFloat = 0; 123 | 124 | $secondsFloat += $hour * self::SECONDS_PER_HOUR; 125 | $secondsFloat += $minute * self::SECONDS_PER_MINUTE; 126 | $secondsFloat += $second; 127 | $secondsFloat += $millisecond / self::MILLISECONDS_PER_SECOND; 128 | 129 | return static::fromSeconds($secondsFloat); 130 | } 131 | 132 | /** 133 | * Creates a new instance from the specified relative position within the given time frame in seconds 134 | * 135 | * @param float $runtimeStartSecondsFloat the start time of playback in seconds 136 | * @param float $runtimeEndSecondsFloat the end time of playback in seconds 137 | * @param float $position the relative position within the playback frame (between `0.0` and `1.0`) 138 | * @return static a new instance of this class 139 | */ 140 | public static function fromPositionInRuntime($runtimeStartSecondsFloat, $runtimeEndSecondsFloat, $position) { 141 | return new static($runtimeStartSecondsFloat + ($runtimeEndSecondsFloat - $runtimeStartSecondsFloat) * $position); 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /Website/app/Lib/Filter.php: -------------------------------------------------------------------------------- 1 | startTime = $startTime; 42 | $this->endTime = $endTime; 43 | $this->annotations = []; 44 | } 45 | 46 | /** 47 | * Returns the start time of this instance 48 | * 49 | * @return Timestamp the time 50 | */ 51 | public function getStartTime() { 52 | return $this->startTime; 53 | } 54 | 55 | /** 56 | * Returns the end time of this instance 57 | * 58 | * @return Timestamp the time 59 | */ 60 | public function getEndTime() { 61 | return $this->endTime; 62 | } 63 | 64 | /** 65 | * Returns the list of annotations from this instance 66 | * 67 | * @return Annotation[]|ContentualAnnotation[]|\App\Lib\Mcf\Annotation[] the list of annotations 68 | */ 69 | public function getAnnotations() { 70 | return $this->annotations; 71 | } 72 | 73 | /** 74 | * Adds a new annotation to this instance 75 | * 76 | * @param Annotation $annotation the annotation to add 77 | */ 78 | public function addAnnotation(Annotation $annotation) { 79 | $this->annotations[] = $annotation; 80 | } 81 | 82 | /** 83 | * Changes the start and end time of this instance and re-scales all other time references accordingly 84 | * 85 | * @param Timestamp $start the new start time of this instance 86 | * @param Timestamp $end the new end time of this instance 87 | */ 88 | public function changeTime(Timestamp $start, Timestamp $end) { 89 | // retrieve the old and new offset (i.e. the start times) 90 | $oldOffset = $this->startTime->toSeconds(); 91 | $newOffset = $start->toSeconds(); 92 | 93 | // calculate the old and new length (i.e. the total durations) 94 | $oldLength = $this->endTime->toSeconds() - $oldOffset; 95 | $newLength = $end->toSeconds() - $newOffset; 96 | 97 | // for each annotation in this instance 98 | foreach ($this->annotations as $annotation) { 99 | // revert the old offset 100 | $annotation->getTiming()->subtractSeconds($oldOffset); 101 | 102 | // revert the old duration 103 | $annotation->getTiming()->divide($oldLength); 104 | 105 | // apply the new duration 106 | $annotation->getTiming()->multiply($newLength); 107 | 108 | // apply the new offset 109 | $annotation->getTiming()->addSeconds($newOffset); 110 | } 111 | 112 | // update the start and end time 113 | $this->startTime = $start; 114 | $this->endTime = $end; 115 | } 116 | 117 | /** Normalizes the start and end time of this instance and re-scales all other time references accordingly */ 118 | public function normalizeTime() { 119 | $this->changeTime( 120 | static::createTimestampFromSeconds(self::START_TIME_DEFAULT), 121 | static::createTimestampFromSeconds(self::END_TIME_DEFAULT) 122 | ); 123 | } 124 | 125 | /** 126 | * Creates a new timestamp from the specified time in seconds 127 | * 128 | * @param float $secondsFloat the time in seconds 129 | * @return Timestamp the new instance 130 | */ 131 | protected static function createTimestampFromSeconds($secondsFloat) { 132 | return Timestamp::fromSeconds($secondsFloat); 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /Website/public/css/custom.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * MovieContentFilter (https://www.moviecontentfilter.com/) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the GNU AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.txt) 5 | */ 6 | 7 | body { 8 | padding-top: 50px; 9 | padding-bottom: 20px; 10 | } 11 | .breadcrumb > li + li:before { 12 | color: #888; 13 | content: "\203a\00a0"; 14 | } 15 | .im-delight-moviecontentfilter-use-cases { 16 | margin-top: 16px; 17 | } 18 | div.page-header { 19 | margin-top: 28px; 20 | } 21 | .breadcrumb { 22 | margin-bottom: 0; 23 | } 24 | body > div.container div.btn-group, body > div.container div.btn-group-vertical { 25 | margin-top: 8px; 26 | margin-bottom: 16px; 27 | } 28 | div.jumbotron .btn { 29 | margin-top: 10px; 30 | margin-bottom: 10px; 31 | } 32 | 33 | /* Patch `abbr` appearance to prevent browsers from showing double underline */ 34 | abbr[title] { 35 | border-bottom: 1px dotted; 36 | text-decoration: none; 37 | /* 38 | going forward, the following solution will be preferable as soon as browser 39 | support for the `text-decoration` shorthand with two values has improved 40 | border-bottom: none; 41 | text-decoration: underline; 42 | text-decoration: underline dotted; 43 | */ 44 | } 45 | 46 | select.im-delight-moviecontentfilter-severity-1, select.im-delight-moviecontentfilter-severity-1 option { 47 | background-color: #ff6680; 48 | color: #fff; 49 | } 50 | select.im-delight-moviecontentfilter-severity-2, select.im-delight-moviecontentfilter-severity-2 option { 51 | background-color: #ffb380; 52 | } 53 | select.im-delight-moviecontentfilter-severity-3, select.im-delight-moviecontentfilter-severity-3 option { 54 | background-color: #ffff80; 55 | } 56 | 57 | /* Optionally center labels inside progress bars and make sure they're readable, no matter on which background */ 58 | div.progress.progress-center-label { 59 | position: relative; 60 | } 61 | div.progress.progress-center-label span { 62 | position: absolute; 63 | left: 0; 64 | top: 0; 65 | display: block; 66 | width: 100%; 67 | color: #fff; 68 | } 69 | div.progress.progress-dark { 70 | background-color: #c2c2c2; 71 | } 72 | div.progress.progress-dark { 73 | background-image: -webkit-linear-gradient(top, #b8b8b8 0, #c2c2c2 100%); 74 | background-image: -o-linear-gradient(top, #b8b8b8 0, #c2c2c2 100%); 75 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #b8b8b8), to(#c2c2c2)); 76 | background-image: linear-gradient(to bottom, #b8b8b8 0, #c2c2c2 100%); 77 | background-repeat: repeat-x; 78 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffb8b8b8', endColorstr='#ffc2c2c2', GradientType=0); 79 | } 80 | 81 | /* Improve display of privacy policy by adjusting certain properties of definition/description lists */ 82 | .im-delight-moviecontentfilter-privacy-policy dl { 83 | margin-top: 0; 84 | margin-bottom: 16px; 85 | } 86 | .im-delight-moviecontentfilter-privacy-policy dl dl { 87 | margin-left: 24px; 88 | } 89 | .im-delight-moviecontentfilter-privacy-policy dd { 90 | margin-bottom: 8px; 91 | margin-left: 0; 92 | } 93 | .im-delight-moviecontentfilter-privacy-policy dl dl dl dd { 94 | margin-bottom: 0; 95 | } 96 | 97 | /* Remove visual hints that elements representing contact information with accessibility targets are links */ 98 | .im-delight-moviecontentfilter-contact-information a, 99 | .im-delight-moviecontentfilter-privacy-policy > dl > dd:last-child > a, 100 | .im-delight-moviecontentfilter-privacy-policy > dl > dd:last-child > a > img { 101 | cursor: default; 102 | } 103 | 104 | /* Contextual color for progress bars that is somewhere between "warning" and "danger" */ 105 | .progress-bar-caution { 106 | background-color: #e5884f; 107 | } 108 | .progress-bar-caution { 109 | background-image: -webkit-linear-gradient(top, #e5884f 0, #db7026 100%); 110 | background-image: -o-linear-gradient(top, #e5884f 0, #db7026 100%); 111 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #e5884f), to(#db7026)); 112 | background-image: linear-gradient(to bottom, #e5884f 0, #db7026 100%); 113 | background-repeat: repeat-x; 114 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe5884f', endColorstr='#ffdb7026', GradientType=0); 115 | } 116 | 117 | /* Variant of progress bar for transparent segments in a stacked group */ 118 | .progress-bar-transparent { 119 | background-color: transparent; 120 | background-image: none; 121 | box-shadow: none; 122 | } 123 | 124 | /* Removes any border, outline or shadow from the progress bar that this is applied to */ 125 | .progress-bar-borderless { 126 | box-shadow: none; 127 | } 128 | -------------------------------------------------------------------------------- /Website/views/contribute.html: -------------------------------------------------------------------------------- 1 | {# 2 | * MovieContentFilter (https://www.moviecontentfilter.com/) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the GNU AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.txt) 5 | #} 6 | {% include 'includes/page_header.html' %} 7 | 8 | 9 | 10 | 11 |
    12 |

    13 | 14 | {% if title %}“{{ title }}”{% elseif series %}Season {{ series.season }}, Episode {{ series.episodeInSeason }}{% else %}Untitled{% endif %} 15 | ({{ year }}) 16 | 17 |

    18 |

    19 | Do you have a legal copy of {% if title %}“{{ title }}”{% elseif series %}season {{ series.season }}, episode {{ series.episodeInSeason }}{% else %}the work{% endif %} on your device’s storage? 20 |
    21 | Start annotating it with filterable categories now, locally, which will help 22 |
    23 | you and other movie fans remove unwanted scenes from your respective versions in seconds. 24 |

    25 |

    26 | Always use the official release of a movie or TV show to create the filter. 27 |
    28 | If available, use the uncut or uncensored version of the source. 29 |
    30 | Hiding unwanted scenes is what can later be done by every viewer individually. 31 |

    32 | 35 | 36 |
    37 | 86 | {% include 'includes/modals/contribute/select_topic.html' %} 87 | {% include 'includes/modals/contribute/select_category.html' %} 88 | {% include 'includes/modals/contribute/select_severity.html' %} 89 | {% include 'includes/modals/contribute/select_channel.html' %} 90 | {% include 'includes/modals/contribute/select_next_action.html' %} 91 | {% include 'includes/scripts.html' %} 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /Website/public/css/contribute.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * MovieContentFilter (https://www.moviecontentfilter.com/) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the GNU AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.txt) 5 | */ 6 | 7 | * { 8 | box-sizing: border-box; 9 | } 10 | html, 11 | body, 12 | .screen { 13 | width: 100%; 14 | height: 100%; 15 | min-width: 100%; 16 | min-height: 100%; 17 | } 18 | body { 19 | background-color: #007272; 20 | } 21 | a, 22 | a:link, 23 | a:visited, 24 | a:hover, 25 | a:focus, 26 | a:active { 27 | color: inherit; 28 | text-decoration: none; 29 | } 30 | a:hover { 31 | text-decoration: underline; 32 | } 33 | .text-light-gray { 34 | color: #eee; 35 | } 36 | .text-light-gray a { 37 | color: #fff; 38 | font-weight: bold; 39 | } 40 | .text-dark-gray { 41 | color: #bbb; 42 | } 43 | .text-dark-gray a { 44 | color: #ccc; 45 | font-weight: bold; 46 | } 47 | .text-center { 48 | text-align: center; 49 | } 50 | a.com-moviecontentfilter-button, 51 | button.com-moviecontentfilter-button { 52 | display: inline-block; 53 | text-align: center; 54 | } 55 | a.com-moviecontentfilter-button-large, 56 | button.com-moviecontentfilter-button-large { 57 | height: 64px; 58 | } 59 | a.com-moviecontentfilter-button-center, 60 | button.com-moviecontentfilter-button-center { 61 | display: block; 62 | width: 50%; 63 | } 64 | a.com-moviecontentfilter-button:hover, 65 | button.com-moviecontentfilter-button:hover { 66 | color: #000; 67 | } 68 | button.com-moviecontentfilter-button:disabled { 69 | color: #aaa; 70 | } 71 | .screen { 72 | min-width: 100%; 73 | min-height: 100%; 74 | } 75 | .screen h1, 76 | #screen-intro p { 77 | position: absolute; 78 | left: 0; 79 | display: block; 80 | width: 100%; 81 | } 82 | .screen h1 { 83 | top: 10%; 84 | font-size: 28px; 85 | } 86 | .screen h1 small { 87 | font-size: inherit; 88 | color: inherit; 89 | } 90 | #screen-intro p { 91 | font-size: 120%; 92 | line-height: 1.25; 93 | margin: 0; 94 | } 95 | #screen-intro p:nth-child(2) { 96 | top: 25%; 97 | } 98 | #screen-intro p:nth-child(3) { 99 | top: 50%; 100 | } 101 | body.contribute button#video-source { 102 | position: absolute; 103 | top: 75%; 104 | left: 25%; 105 | } 106 | .hidden { 107 | display: none; 108 | } 109 | body.contribute #screen-annotate { 110 | /* width of `#playback-controls` */ 111 | padding-left: 72px; 112 | /* height of `#annotation-controls` */ 113 | padding-bottom: 48px; 114 | } 115 | body.contribute #screen-annotate #playback-controls { 116 | width: 72px; 117 | height: 100%; 118 | 119 | /* negative width of this element */ 120 | margin-left: -72px; 121 | 122 | /* sum of heights of `.indicator` */ 123 | padding-bottom: 72px; 124 | 125 | float: left; 126 | } 127 | body.contribute #screen-annotate #playback-controls span.indicator { 128 | display: inline-block; 129 | width: 100%; 130 | 131 | /* space for these elements' heights is reserved in `#playback-controls` by adding the sum of these heights as padding there */ 132 | height: 24px; 133 | 134 | margin: 0; 135 | 136 | /* same as height */ 137 | line-height: 24px; 138 | 139 | text-align: center; 140 | vertical-align: middle; 141 | color: #fff; 142 | } 143 | body.contribute #screen-annotate #playback-controls #progress-indicator { 144 | font-size: 90%; 145 | } 146 | body.contribute #screen-annotate #playback-controls button { 147 | display: block; 148 | width: 100%; 149 | 150 | /* height of `#playback-controls` distributed evenly among these buttons (treating buttons with twice the height as two buttons) */ 151 | height: 12.5%; 152 | 153 | margin: 0; 154 | overflow: hidden; 155 | } 156 | body.contribute #screen-annotate #playback-controls button:hover { 157 | color: #000; 158 | } 159 | body.contribute #screen-annotate #playback-controls button:disabled { 160 | color: #aaa; 161 | } 162 | body.contribute #screen-annotate #playback-controls #play { 163 | /* twice the normal height of these buttons */ 164 | height: 25%; 165 | } 166 | body.contribute #screen-annotate video { 167 | display: block; 168 | width: 100%; 169 | height: 100%; 170 | float: left; 171 | } 172 | body.contribute #screen-annotate::after { 173 | content: ""; 174 | display: block; 175 | clear: both; 176 | } 177 | body.contribute #screen-annotate #annotation-controls { 178 | height: 48px; 179 | 180 | /* negative height of this element */ 181 | margin-bottom: -48px; 182 | } 183 | body.contribute #screen-annotate #annotation-controls button { 184 | display: block; 185 | height: 100%; 186 | margin: 0; 187 | float: left; 188 | } 189 | body.contribute #screen-annotate #annotation-controls button:hover { 190 | color: #000; 191 | } 192 | body.contribute #screen-annotate #annotation-controls button:disabled { 193 | color: #aaa; 194 | } 195 | body.contribute #screen-annotate #annotation-controls::after { 196 | content: ""; 197 | display: block; 198 | clear: both; 199 | } 200 | -------------------------------------------------------------------------------- /Website/public/js/custom.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * MovieContentFilter (https://www.moviecontentfilter.com/) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the GNU AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.txt) 5 | */ 6 | 7 | "use strict"; 8 | 9 | function setSecondaryWorkTypeEpisode(episode) { 10 | // don't require the title information for episodes of a series 11 | $("#title").prop("required", !episode); 12 | 13 | // set a meaningful placeholder for a series or for an episode 14 | $("#title").attr("placeholder", $("#title").data(episode ? "placeholder-episode" : "placeholder-series")); 15 | 16 | // change the opacity of the title field to show that it becomes optional for episodes 17 | $(".im-delight-moviecontentfilter-title-container").fadeTo(400, episode ? 0.5 : 1.0); 18 | 19 | var fieldNames = [ "parent", "season", "episode" ]; 20 | 21 | var field; 22 | 23 | for (var i = 0; i < fieldNames.length; i++) { 24 | // get a reference to the containers of the additional fields 25 | field = $(".im-delight-moviecontentfilter-"+fieldNames[i]+"-container"); 26 | 27 | // for episodes of a series 28 | if (episode) { 29 | // show the additional fields 30 | field.slideDown(400); 31 | } 32 | // for all other works 33 | else { 34 | // hide the additional fields 35 | field.slideUp(400); 36 | } 37 | 38 | // now get a reference to the additional fields themselves 39 | field = $("#"+fieldNames[i]); 40 | 41 | // require the additional fields for episodes of a series only 42 | field.prop("required", episode); 43 | 44 | // disable them otherwise 45 | field.prop("disabled", !episode); 46 | } 47 | } 48 | 49 | function updateSeverityIndication(field, level) { 50 | field = $(field); 51 | level = Number(level); 52 | 53 | var className; 54 | 55 | for (var i = 1; i <= 3; i++) { 56 | className = "im-delight-moviecontentfilter-severity-"+i; 57 | 58 | if (i === level) { 59 | field.addClass(className); 60 | } 61 | else { 62 | field.removeClass(className); 63 | } 64 | } 65 | } 66 | 67 | function updateFilterPropertiesElements(formatElementsName, videoSourceContainerId, videoSourceId, modeContainerId, synchronizationContainerId) { 68 | var selectedFormat = $("input[name="+formatElementsName+"]:checked").val(); 69 | var videoSourceContainer = $("#"+videoSourceContainerId); 70 | var videoSource = $("#"+videoSourceId); 71 | var modeContainer = $("#"+modeContainerId); 72 | var synchronizationContainer = $("#"+synchronizationContainerId); 73 | 74 | if (selectedFormat === 'xspf' || selectedFormat === 'm3u') { 75 | videoSourceContainer.slideDown(400); 76 | videoSource.prop("required", true); 77 | } 78 | else { 79 | videoSourceContainer.slideUp(400); 80 | videoSource.prop("required", false); 81 | } 82 | 83 | if (selectedFormat === 'mcf') { 84 | modeContainer.slideUp(400); 85 | 86 | synchronizationContainer.slideUp(400); 87 | synchronizationContainer.find("input").prop("required", false); 88 | } 89 | else { 90 | modeContainer.slideDown(400); 91 | 92 | synchronizationContainer.slideDown(400); 93 | synchronizationContainer.find("input").prop("required", true); 94 | } 95 | } 96 | 97 | function voteForAnnotation(url, clickedButton, scoreAddend, votingContainerId, scoreContainerId) { 98 | // first disable the clicked button 99 | $(clickedButton).prop("disabled", true); 100 | 101 | $.post({ 102 | type: "POST", 103 | url: url 104 | }).done(function () { 105 | // the user's vote has been cast so we can disable the voting now 106 | $("#"+votingContainerId).find("button").prop("disabled", true); 107 | 108 | // and update the score 109 | var scoreContainer = $("#"+scoreContainerId); 110 | var oldScore = parseInt(scoreContainer.text(), 10); 111 | var newScore = oldScore + scoreAddend; 112 | var newScoreStr = (newScore > 0 ? "+" : "") + newScore; 113 | scoreContainer.text(newScoreStr); 114 | }).fail(function (jqXHR) { 115 | // the user has to sign in first 116 | if (jqXHR.status === 401) { 117 | // tell them what to do 118 | alert("Please sign in to your account in order to vote!\n\nYou can find the login on the top of this page.\n\nThank you!"); 119 | } 120 | // any other error occurred 121 | else { 122 | // we've probably just lost the connection 123 | alert("Please check your internet connection!"); 124 | } 125 | 126 | // finally re-enable the clicked button 127 | $(clickedButton).prop("disabled", false); 128 | }); 129 | } 130 | 131 | // when the DOM is ready 132 | $(document).ready(function () { 133 | // if we're on the page for adding a new episode to a series 134 | if (window.location.href.indexOf("/add?primary-type=series&secondary-type=episode") !== -1) { 135 | // try to get a reference to the option that enables the episode mode 136 | var episodeModeOption = $("#secondary-type-episode"); 137 | // if that option exists 138 | if (episodeModeOption.length) { 139 | // check it 140 | episodeModeOption.click(); 141 | } 142 | } 143 | }); 144 | -------------------------------------------------------------------------------- /Website/views/settings.html: -------------------------------------------------------------------------------- 1 | {# 2 | * MovieContentFilter (https://www.moviecontentfilter.com/) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the GNU AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.txt) 5 | #} 6 | {% include 'includes/page_top.html' %} 7 | 11 |
    12 |
    13 |
    14 | 15 | 21 | 22 |
    23 | {% include 'includes/old_password_1x.html' with { 'id': 'old-password', 'name': 'old-password', 'isReset': true } only %} 24 | {% include 'includes/new_password_2x.html' with { 'isReset': true, 'passwordMinLength': passwordMinLength } only %} 25 |
    26 |
    27 | 28 |
    29 |
    30 |
    31 | 32 |
    33 | {% include 'includes/old_password_1x.html' with { 'id': 'email-password', 'name': 'email-password', 'isReset': false } only %} 34 |
    35 | 36 |
    37 | 38 |

    Please enter your new email address here. You will use this to sign in afterwards. Make sure you can receive emails at this address, please.

    39 |
    40 |
    41 |
    42 | 43 |
    44 | 45 |

    Please repeat your email address here, just to prevent any typing errors.

    46 |
    47 |
    48 |
    49 |
    50 | 51 |
    52 |
    53 |
    54 | 55 |
    56 | {% include 'includes/old_password_1x.html' with { 'id': 'password-reset-password', 'name': 'password-reset-password', 'isReset': false } only %} 57 |
    58 |
    59 |
    60 | 63 |
    64 |
    65 |
    66 |
    67 |
    68 | 69 |
    70 |
    71 |
    72 | 73 |
    74 |
    75 |
    76 |
    77 | 80 |
    81 |
    82 |
    83 |
    84 |
    85 | 86 |
    87 |
    88 |
    89 |
    90 |
    91 |
    92 | {% include 'includes/page_bottom.html' %} 93 | -------------------------------------------------------------------------------- /Website/views/works_add_step_2.html: -------------------------------------------------------------------------------- 1 | {# 2 | * MovieContentFilter (https://www.moviecontentfilter.com/) 3 | * Copyright (c) delight.im (https://www.delight.im/) 4 | * Licensed under the GNU AGPL v3 (https://www.gnu.org/licenses/agpl-3.0.txt) 5 | #} 6 | {% include 'includes/page_top.html' %} 7 | 12 |
    13 |
    14 |
    15 | 16 |
    17 | 18 | {% if primaryType == 'series' %} 19 |
    20 | 21 |
    22 |
    23 | 27 |
    28 |
    29 | 33 |
    34 |
    35 |
    36 | {% endif %} 37 |
    38 | 39 |
    40 | 41 |

    Please enter the work’s original title from its country of origin. If in doubt, you may search online, e.g. on IMDb. Do not use any translated titles here, please. Our filters work for every language, so let’s keep this collection free from language barriers and borders.

    42 |
    43 |
    44 |
    45 | 46 |
    47 | 48 |

    Do you know the year of first publication? If not, please look it up, e.g. on IMDb. Thank you! Providing the year helps everybody search the collection faster and find available movies and TV shows more easily.

    49 |
    50 |
    51 | {% if primaryType == 'series' %} 52 | {% if seriesParents %} 53 | 65 | {% endif %} 66 | 78 | 90 | {% endif %} 91 |
    92 | 93 |
    94 | 95 |

    Please enter the address (URL) of this title’s IMDb page. This will allow other people to look up additional information easily.

    96 |
    97 |
    98 |
    99 |
    100 | 101 |
    102 |
    103 |
    104 |
    105 |
    106 |
    107 | {% include 'includes/page_bottom.html' %} 108 | -------------------------------------------------------------------------------- /Website/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ### PHP-Foundation (https://github.com/delight-im/PHP-Foundation) 4 | ### Copyright (c) delight.im (https://www.delight.im/) 5 | ### Licensed under the MIT License (https://opensource.org/licenses/MIT) 6 | 7 | # Switch to the directory where the current script is located 8 | cd "${BASH_SOURCE%/*}" || exit 1 9 | 10 | # If no command-line arguments (or only incomplete ones) have been provided 11 | if [ "$1" == "" ] || [ "$2" == "" ] || [ "$3" == "" ] || [ "$4" == "" ]; then 12 | # Explain command 13 | echo "Usage:" 14 | echo " deploy.sh " 15 | echo " : hostname of the target server, e.g. 'example.com'" 16 | echo " : SSH port at the target server, e.g. '22'" 17 | echo " : user for authentication at the target server, e.g. 'john-doe'" 18 | echo " : application directory on the target server (without trailing slash), e.g. '/var/www/example.com'" 19 | 20 | # If no command-line arguments have been specified at all 21 | if [ "$1" == "" ] && [ "$2" == "" ] && [ "$3" == "" ] && [ "$4" == "" ]; then 22 | # Return with success 23 | exit 0 24 | # If the command-line arguments have just been incomplete 25 | else 26 | # Return with failure 27 | exit 2 28 | fi 29 | fi 30 | 31 | # BEGIN CONSTANTS 32 | 33 | # Hostname of the target server as received with the command-line arguments 34 | TARGET_SSH_HOST=$1 35 | # SSH port at the target server as received with the command-line arguments 36 | TARGET_SSH_PORT=$2 37 | # User for authentication at the target server as received with the command-line arguments 38 | TARGET_SSH_USER=$3 39 | # Application directory on the target server (without trailing slash) as received with the command-line arguments 40 | TARGET_APPLICATION_PATH=$4 41 | # Unique name for the new deployment 42 | DEPLOYMENT_NAME="deployment-$(date -u +'%Y%m%dT%H%M%SZ')" 43 | # Filename used for archives of the new deployment 44 | DEPLOYMENT_ARCHIVE_FILENAME="$DEPLOYMENT_NAME.tar.gz" 45 | 46 | # END CONSTANTS 47 | 48 | # Introduce the deployment to the user and confirm the target host and directory 49 | echo "Deploying to '$TARGET_APPLICATION_PATH' on '$TARGET_SSH_HOST'" 50 | 51 | # Verify that the source directory is a valid project root by looking for some important files and directories 52 | if [ -d "app" ] && [ -f "index.php" ] && [ -d "public" ] && [ -d "vendor" ] && [ -d "views" ]; then 53 | echo " * Verified source directory ..." 54 | else 55 | echo " * Source directory could not be verified ..." 56 | exit 3 57 | fi 58 | 59 | # Create an archive of all files in the source directory that are to be transferred to the target host 60 | echo " * Packing files in source directory ..." 61 | echo " * Ignoring '.htaccess' file (environment-specific) ..." # [1] 62 | echo " * Ignoring 'backups' directory (environment-specific) ..." # [1] 63 | echo " * Ignoring 'backup.sh' file (protected) ..." # [1] 64 | echo " * Ignoring 'config' directory (environment-specific) ..." # [1] 65 | echo " * Ignoring 'deploy.sh' file (needless) ..." # [1] 66 | echo " * Ignoring 'storage/app' directory (environment-specific) ..." # [1] 67 | echo " * Ignoring 'storage/framework' directory (environment-specific) ..." # [1] 68 | touch "$DEPLOYMENT_ARCHIVE_FILENAME" 69 | tar \ 70 | --create \ 71 | --gzip \ 72 | --exclude "./$DEPLOYMENT_ARCHIVE_FILENAME" \ 73 | --exclude "./.git" \ 74 | --exclude "./.idea" \ 75 | --exclude "./.htaccess" \ 76 | --exclude "./backups" \ 77 | --exclude "./backup.sh" \ 78 | --exclude "./config" \ 79 | --exclude "./deploy.sh" \ 80 | --exclude "./storage/app" \ 81 | --exclude "./storage/framework" \ 82 | --file="$DEPLOYMENT_ARCHIVE_FILENAME" \ 83 | . # [1] 84 | 85 | # Transfer the generated archive to the target host and delete it from the source directory afterwards 86 | echo " * Moving packed files from source to target host ..." 87 | scp -q -P "$TARGET_SSH_PORT" "$DEPLOYMENT_ARCHIVE_FILENAME" "${TARGET_SSH_USER}@${TARGET_SSH_HOST}:$TARGET_APPLICATION_PATH" 88 | rm "$DEPLOYMENT_ARCHIVE_FILENAME" 89 | 90 | # Establish an SSH connection to the target host 91 | ssh -p "$TARGET_SSH_PORT" "${TARGET_SSH_USER}@${TARGET_SSH_HOST}" /bin/bash <<- EOF 92 | # Verify that the target directory exists, and, if found, switch to that directory 93 | if [ -d "$TARGET_APPLICATION_PATH" ]; then 94 | cd "$TARGET_APPLICATION_PATH" 95 | fi 96 | 97 | # Verify that the target directory is now the active working directory 98 | if [ "\$PWD" = "$TARGET_APPLICATION_PATH" ]; then 99 | echo " * Found target directory ..." 100 | else 101 | echo " * Target directory could not be found ..." 102 | exit 4 103 | fi 104 | 105 | # Verify that the target directory is a valid project root by looking for some important files and directories 106 | if [ -f ".htaccess" ] && [ -d "storage" ]; then 107 | echo " * Verified target directory ..." 108 | else 109 | echo " * Target directory could not be verified ..." 110 | exit 5 111 | fi 112 | 113 | # Enable maintenance mode on the site 114 | echo " * Enabling maintenance mode ..." 115 | sed -i 's/^\t# RewriteRule . maintenance.php \[END]/\tRewriteRule . maintenance.php [END]/m' .htaccess 116 | 117 | # Delete all directories and most files that new versions will subsequently be deployed for 118 | echo " * Cleaning up old files ..." 119 | find . \ 120 | -depth \ 121 | \! -path "./$DEPLOYMENT_ARCHIVE_FILENAME" \ 122 | \! -path './index.php' \ 123 | \! -path './maintenance.php' \ 124 | \! -path './.htaccess' \ 125 | \! -path './backups' \ 126 | \! -path './backups/*' \ 127 | \! -path './backup.sh' \ 128 | \! -path './config' \ 129 | \! -path './config/*' \ 130 | \! -path './deploy.sh' \ 131 | \! -path './storage' \ 132 | \! -path './storage/app' \ 133 | \! -path './storage/app/*' \ 134 | \! -path './storage/framework' \ 135 | \! -path './storage/framework/*' \ 136 | -delete # [1] 137 | 138 | # Extract the transferred archive in the target directory and delete the archive afterwards 139 | echo " * Unpacking files in target directory ..." 140 | tar --extract --gzip --overwrite --file="$DEPLOYMENT_ARCHIVE_FILENAME" 141 | rm "$DEPLOYMENT_ARCHIVE_FILENAME" 142 | 143 | # Disable maintenance mode on the site again 144 | echo " * Disabling maintenance mode ..." 145 | sed -i 's/^\tRewriteRule . maintenance.php \[END]/\t# RewriteRule . maintenance.php [END]/m' .htaccess 146 | 147 | # Announce that deployment has finished 148 | echo 'Done' 149 | EOF 150 | 151 | # [1] The entries in the set of ignored files should be kept consistent in all places 152 | -------------------------------------------------------------------------------- /Website/app/Lib/Mcf/Mcf.php: -------------------------------------------------------------------------------- 1 | version = $version; 64 | } 65 | 66 | /** 67 | * Converts this instance to a string representation 68 | * 69 | * @return string the string representation 70 | */ 71 | public function __toString() { 72 | $lines = []; 73 | 74 | $lines[] = 'WEBVTT MovieContentFilter ' . ((string) $this->version); 75 | $lines[] = ''; 76 | $lines[] = 'NOTE'; 77 | $lines[] = 'TITLE ' . (string) $this->metaTitle; 78 | $lines[] = 'YEAR ' . (string) $this->metaYear; 79 | $lines[] = 'TYPE ' . (string) $this->metaType; 80 | $lines[] = 'SEASON ' . (string) $this->metaSeason; 81 | $lines[] = 'EPISODE ' . (string) $this->metaEpisode; 82 | $lines[] = 'SOURCE ' . (string) $this->metaSource; 83 | $lines[] = 'IMDB ' . (string) $this->metaImdb; 84 | $lines[] = 'RELEASE ' . (string) $this->metaRelease; 85 | $lines[] = 'COMMENT ' . (string) $this->metaComment; 86 | $lines[] = ''; 87 | $lines[] = 'NOTE'; 88 | $lines[] = 'START ' . ((string) $this->startTime); 89 | $lines[] = 'END ' . ((string) $this->endTime); 90 | $lines[] = ''; 91 | 92 | foreach ($this->annotations as $annotation) { 93 | $lines[] = (string) $annotation; 94 | } 95 | 96 | return \implode("\n", $lines); 97 | } 98 | 99 | /** 100 | * Parses an instance from the specified string 101 | * 102 | * @param string $str the string to parse 103 | * @return static a new instance of this class 104 | * @throws EmptyContainerException 105 | * @throws InvalidContainerException 106 | * @throws InvalidFileEndTimeException 107 | * @throws InvalidFileStartTimeException 108 | */ 109 | public static function parse($str) { 110 | if (\preg_match(self::CONTAINER_REGEX, $str, $container)) { 111 | $version = Version::parse($container[1]); 112 | 113 | try { 114 | $fileStartTime = WebvttTimestamp::parse($container[2]); 115 | } 116 | catch (InvalidWebvttTimestampException $e) { 117 | throw new InvalidFileStartTimeException(); 118 | } 119 | 120 | try { 121 | $fileEndTime = WebvttTimestamp::parse($container[3]); 122 | } 123 | catch (InvalidWebvttTimestampException $e) { 124 | throw new InvalidFileEndTimeException(); 125 | } 126 | 127 | $out = new static($version, $fileStartTime, $fileEndTime); 128 | 129 | if (\preg_match_all(self::ANNOTATION_BLOCKS_REGEX, $container[4], $annotationBlocks)) { 130 | foreach ($annotationBlocks[1] as $annotationBlock) { 131 | $out->addAnnotation(Annotation::parse($annotationBlock)); 132 | } 133 | } 134 | else { 135 | throw new EmptyContainerException(); 136 | } 137 | 138 | return $out; 139 | } 140 | else { 141 | throw new InvalidContainerException(); 142 | } 143 | } 144 | 145 | /** 146 | * @param string $metaTitle 147 | */ 148 | public function setMetaTitle($metaTitle) { 149 | $this->metaTitle = $metaTitle; 150 | } 151 | 152 | /** 153 | * @param int $metaYear 154 | */ 155 | public function setMetaYear($metaYear) { 156 | $this->metaYear = $metaYear; 157 | } 158 | 159 | /** 160 | * @param string $metaType 161 | */ 162 | public function setMetaType($metaType) { 163 | $this->metaType = $metaType; 164 | } 165 | 166 | /** 167 | * @param int $metaSeason 168 | */ 169 | public function setMetaSeason($metaSeason) { 170 | $this->metaSeason = $metaSeason; 171 | } 172 | 173 | /** 174 | * @param int $metaEpisode 175 | */ 176 | public function setMetaEpisode($metaEpisode) { 177 | $this->metaEpisode = $metaEpisode; 178 | } 179 | 180 | /** 181 | * @param string $metaSource 182 | */ 183 | public function setMetaSource($metaSource) { 184 | $this->metaSource = $metaSource; 185 | } 186 | 187 | /** 188 | * @param string $metaImdb 189 | */ 190 | public function setMetaImdb($metaImdb) { 191 | $this->metaImdb = $metaImdb; 192 | } 193 | 194 | /** 195 | * @param string $metaRelease 196 | */ 197 | public function setMetaRelease($metaRelease) { 198 | $this->metaRelease = $metaRelease; 199 | } 200 | 201 | /** 202 | * @param string $metaComment 203 | */ 204 | public function setMetaComment($metaComment) { 205 | $this->metaComment = $metaComment; 206 | } 207 | 208 | /** 209 | * Creates a new timestamp from the specified time in seconds 210 | * 211 | * @param float $secondsFloat the time in seconds 212 | * @return WebvttTimestamp the new instance 213 | */ 214 | protected static function createTimestampFromSeconds($secondsFloat) { 215 | return WebvttTimestamp::fromSeconds($secondsFloat); 216 | } 217 | 218 | } 219 | -------------------------------------------------------------------------------- /Website/app/SettingsController.php: -------------------------------------------------------------------------------- 1 | view( 27 | 'settings.html', 28 | [ 29 | 'passwordMinLength' => AuthController::MIN_PASSWORD_LENGTH, 30 | 'passwordResetEnabled' => $app->auth()->isPasswordResetEnabled() 31 | ] 32 | ); 33 | } 34 | 35 | public static function postChangePassword(App $app) { 36 | self::ensureAuthenticated($app); 37 | 38 | $oldPassword = $app->input()->post('old-password', \TYPE_STRING); 39 | $newPassword = $app->input()->post('password-1', \TYPE_STRING); 40 | $newPasswordRepeated = $app->input()->post('password-2', \TYPE_STRING); 41 | 42 | if ($oldPassword !== null && $newPassword !== null && $newPasswordRepeated !== null) { 43 | if (\strlen($newPassword) >= AuthController::MIN_PASSWORD_LENGTH) { 44 | if ($newPasswordRepeated === $newPassword) { 45 | try { 46 | $app->auth()->changePassword($oldPassword, $newPassword); 47 | 48 | // inform the user about this critical change via email 49 | self::sendEmail( 50 | $app, 51 | 'mail/en-US/password-changed.txt', 52 | 'Your password has been changed', 53 | $app->auth()->getEmail(), 54 | $app->auth()->getUsername(), 55 | [ 56 | 'requestedByIpAddress' => $app->getClientIp(), 57 | 'reasonForEmailDelivery' => 'You’re receiving this email because the password for your account has recently been changed. Your email address is the address associated with that account.' 58 | ] 59 | ); 60 | 61 | $app->flash()->success('Your password has been changed successfully.'); 62 | $app->redirect('/settings'); 63 | } 64 | catch (NotLoggedInException $e) { 65 | self::failNotSignedIn($app); 66 | } 67 | catch (InvalidPasswordException $e) { 68 | $app->flash()->warning('It seems the old password that you entered was not correct. Please try again!'); 69 | $app->redirect('/settings'); 70 | } 71 | catch (TooManyRequestsException $e) { 72 | $app->flash()->warning('Please try again later!'); 73 | $app->redirect('/settings'); 74 | } 75 | } 76 | else { 77 | $app->flash()->warning('The two new passwords didn’t match. Please try again!'); 78 | $app->redirect('/settings'); 79 | } 80 | } 81 | else { 82 | $app->flash()->warning('Please check the requirements for the new password and try again.'); 83 | $app->redirect('/settings'); 84 | } 85 | } 86 | else { 87 | $app->flash()->warning('Please enter your old password once and twice your new password.'); 88 | $app->redirect('/settings'); 89 | } 90 | } 91 | 92 | public static function postChangeEmail(App $app) { 93 | self::ensureAuthenticated($app); 94 | 95 | $password = $app->input()->post('email-password', \TYPE_STRING); 96 | $email = $app->input()->post('email-1', \TYPE_EMAIL); 97 | $emailRepeated = $app->input()->post('email-2', \TYPE_EMAIL); 98 | 99 | if ($email !== null && $emailRepeated !== null) { 100 | if ($emailRepeated === $email) { 101 | try { 102 | if ($app->auth()->reconfirmPassword($password)) { 103 | $app->auth()->changeEmail($email, function ($selector, $token) use ($app, $email) { 104 | // build the URL for the confirmation link 105 | $confirmationUrl = $app->url('/confirm/' . \urlencode($selector) . '/' . \urlencode($token)); 106 | 107 | // send the link to the user 108 | self::sendEmail( 109 | $app, 110 | 'mail/en-US/confirm-email.txt', 111 | 'Confirming your email address', 112 | $email, 113 | 114 | // we can’t be sure just yet that the supplied name (if any) is acceptable to the owner of the *new* email address 115 | null, 116 | 117 | [ 118 | 'requestedByIpAddress' => $app->getClientIp(), 119 | 'reasonForEmailDelivery' => 'You’re receiving this email because this email address has been designated as the new address for your account on our website. If that wasn’t you, please ignore this email and accept our excuses.', 120 | 'confirmationUrl' => $confirmationUrl, 121 | ] 122 | ); 123 | }); 124 | 125 | $app->flash()->success('Please check your inbox for a confirmation email soon. As soon as confirmed, your new email address will be active.'); 126 | $app->redirect('/settings'); 127 | } 128 | else { 129 | $app->flash()->warning('It seems the password that you entered was not correct. Please try again!'); 130 | $app->redirect('/settings'); 131 | } 132 | } 133 | catch (InvalidEmailException $e) { 134 | $app->flash()->warning('Please check the email addresses that you’ve entered and try again.'); 135 | $app->redirect('/settings'); 136 | } 137 | catch (UserAlreadyExistsException $e) { 138 | $app->flash()->warning('An account with that email address does already exist. Do you own another account?'); 139 | $app->redirect('/settings'); 140 | } 141 | catch (EmailNotVerifiedException $e) { 142 | $app->flash()->warning('Please verify your old email address first. You should have received an email containing the activation link.'); 143 | $app->redirect('/settings'); 144 | } 145 | catch (NotLoggedInException $e) { 146 | self::failNotSignedIn($app); 147 | } 148 | catch (TooManyRequestsException $e) { 149 | $app->flash()->warning('Please try again later!'); 150 | $app->redirect('/settings'); 151 | } 152 | } 153 | else { 154 | $app->flash()->warning('The two new email addresses didn’t match. Please try again!'); 155 | $app->redirect('/settings'); 156 | } 157 | } 158 | else { 159 | $app->flash()->warning('Please check the email addresses that you’ve entered and try again.'); 160 | $app->redirect('/settings'); 161 | } 162 | } 163 | 164 | public static function postControlPasswordReset(App $app) { 165 | self::ensureAuthenticated($app); 166 | 167 | $password = $app->input()->post('password-reset-password', \TYPE_STRING); 168 | $enabled = $app->input()->post('password-reset-enabled', \TYPE_INT) === 1; 169 | 170 | try { 171 | if ($app->auth()->reconfirmPassword($password)) { 172 | $app->auth()->setPasswordResetEnabled($enabled); 173 | 174 | $app->flash()->success('Your settings for the password reset have been changed successfully.'); 175 | $app->redirect('/settings'); 176 | } 177 | else { 178 | $app->flash()->warning('It seems the password that you entered was not correct. Please try again!'); 179 | $app->redirect('/settings'); 180 | } 181 | } 182 | catch (NotLoggedInException $e) { 183 | self::failNotSignedIn($app); 184 | } 185 | catch (TooManyRequestsException $e) { 186 | $app->flash()->warning('Please try again later!'); 187 | $app->redirect('/settings'); 188 | } 189 | } 190 | 191 | public static function postLogOutEverywhere(App $app) { 192 | self::ensureAuthenticated($app, '/settings'); 193 | 194 | $includeCurrentDevice = $app->input()->post('current-device', \TYPE_INT); 195 | 196 | try { 197 | if ($includeCurrentDevice === 1) { 198 | $app->auth()->logOutEverywhere(); 199 | } 200 | else { 201 | $app->auth()->logOutEverywhereElse(); 202 | } 203 | 204 | $app->flash()->success('You will be logged out from all sessions within the next five minutes.'); 205 | $app->redirect('/'); 206 | } 207 | catch (NotLoggedInException $e) { 208 | self::failNotSignedIn($app, '/settings'); 209 | } 210 | } 211 | 212 | } 213 | --------------------------------------------------------------------------------