Compare commits

..

10 commits

Author SHA1 Message Date
Kyle Maas
c5047d8df8
Fix null bug in More Options button (#913) 2023-11-14 09:24:05 +02:00
Markos Gogoulos
dcbfaca91c
Developer Experience (#911)
local dev environment
2023-11-13 11:13:08 +02:00
Kyle Maas
918df010f5
Fix bug that crashes page if an encoding has a null URL (#912) 2023-11-13 11:09:16 +02:00
Markos Gogoulos
e9739bab45
Feat celery run (#860)
* avoid calling post save signals
* remove stale celery ids that prevent new services from starting
2023-11-10 16:06:17 +02:00
Kyle Maas
e7ce9ef5c0
Add admin action to generate missing encodings for a particular Media (#883)
* Add admin action to generate missing encodings for a particular Media
* Only regenerate the encodings that are missing
2023-11-10 15:41:20 +02:00
Kyle Maas
4829adf110
Add useful fields to the Encodings admin screen (#885) 2023-11-10 15:37:40 +02:00
lavirez
fdff0811a1
cli.py missing f string (#877) 2023-11-10 15:09:22 +02:00
Kyle Maas
92c0ff579a
Add sitemap (#572)
add sitemap.xml
2023-11-10 15:03:36 +02:00
Markos Gogoulos
847cff2b5c license section 2023-11-10 14:30:14 +02:00
Markos Gogoulos
e8d3ff25be
Disable encoding and show only original file (#829)
Disable encoding and show only original file #829
2023-11-10 14:25:10 +02:00
70 changed files with 352 additions and 506 deletions

View file

@ -26,7 +26,7 @@ jobs:
shell: bash
- name: Run Django Tests
run: docker-compose -f docker-compose-dev.yaml exec --env TESTING=True -T web pytest
run: docker-compose -f docker-compose-dev.yaml exec --env TESTING=True -T web pytest
# Run with coverage, saves report on htmlcov dir
# run: docker-compose -f docker-compose-dev.yaml exec --env TESTING=True -T web pytest --cov --cov-report=html --cov-config=.coveragerc

View file

@ -1,5 +1 @@
Yiannis Stergiou - ys.stergiou@gmail.com
Markos Gogoulos - mgogoulos@gmail.com
Swift Ugandan - swiftugandan@gmail.com
Please see https://github.com/mediacms-io/mediacms/graphs/contributors for complete list of contributors to this repository!

View file

@ -11,6 +11,7 @@ RUN mkdir -p /home/mediacms.io/mediacms/{logs} && cd /home/mediacms.io && python
# Install dependencies:
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . /home/mediacms.io/mediacms

View file

@ -10,10 +10,12 @@ ENV PIP_NO_CACHE_DIR=1
RUN mkdir -p /home/mediacms.io/mediacms/{logs} && cd /home/mediacms.io && python3 -m venv $VIRTUAL_ENV
# Install dependencies:
COPY requirements.txt .
COPY requirements.txt .
COPY requirements-dev.txt .
RUN pip install -r requirements-dev.txt
RUN pip install -r requirements.txt
RUN pip install -r requirements-dev.txt
COPY . /home/mediacms.io/mediacms
WORKDIR /home/mediacms.io/mediacms

View file

@ -6,7 +6,7 @@
MediaCMS is a modern, fully featured open source video and media CMS. It is developed to meet the needs of modern web platforms for viewing and sharing media. It can be used to build a small to medium video and media portal within minutes.
MediaCMS is a modern, fully featured open source video and media CMS. It is developed to meet the needs of modern web platforms for viewing and sharing media. It can be used to build a small to medium video and media portal within minutes.
It is built mostly using the modern stack Django + React and includes a REST API.
@ -56,15 +56,15 @@ A demo is available at https://demo.mediacms.io
## Philosophy
We believe there's a need for quality open source web applications that can be used to build community portals and support collaboration.
We believe there's a need for quality open source web applications that can be used to build community portals and support collaboration.
We have three goals for MediaCMS: a) deliver all functionality one would expect from a modern system, b) allow for easy installation and maintenance, c) allow easy customization and addition of features.
We have three goals for MediaCMS: a) deliver all functionality one would expect from a modern system, b) allow for easy installation and maintenance, c) allow easy customization and addition of features.
## License
MediaCMS is released under [GNU Affero General Public License v3.0 license](LICENSE.txt).
Copyright Markos Gogoulos and Yiannis Stergiou
MediaCMS is released under [GNU Affero General Public License v3.0 license](LICENSE.txt).
Copyright Markos Gogoulos.
## Support and paid services
@ -73,9 +73,9 @@ We provide custom installations, development of extra functionality, migration f
## Hardware dependencies
## Hardware considerations
For a small to medium installation, with a few hours of video uploaded daily, and a few hundreds of active daily users viewing content, 4GB Ram / 2-4 CPUs as minimum is ok. For a larger installation with many hours of video uploaded daily, consider adding more CPUs and more Ram.
For a small to medium installation, with a few hours of video uploaded daily, and a few hundreds of active daily users viewing content, 4GB Ram / 2-4 CPUs as minimum is ok. For a larger installation with many hours of video uploaded daily, consider adding more CPUs and more Ram.
In terms of disk space, think of what the needs will be. A general rule is to multiply by three the size of the expected uploaded videos (since the system keeps original versions, encoded versions plus HLS), so if you receive 1G of videos daily and maintain all of them, you should consider a 1T disk across a year (1G * 3 * 365).
@ -99,6 +99,10 @@ There are two ways to run MediaCMS, through Docker Compose and through installin
Visit [Configuration](docs/admins_docs.md#5-configuration) page.
## Information for developers
Check out the new section on the [Developer Experience](docs/dev_exp.md) page
## Documentation
* [Users documentation](docs/user_docs.md) page
@ -115,7 +119,7 @@ This software uses the following list of awesome technologies: Python, Django, D
- **Cinemata** non-profit media, technology and culture organization - https://cinemata.org
- **Critical Commons** public media archive and fair use advocacy network - https://criticalcommons.org
- **Heritales** International Heritage Film Festival - https://stage.heritales.org
- **American Association of Gynecologic Laparoscopists** - https://surgeryu.aagl.org/
## How to contribute
@ -125,9 +129,10 @@ If you like the project, here's a few things you can do
- Suggest us to others that are interested to hire us
- Write a blog post/article about MediaCMS
- Share on social media about the project
- Open issues, participate on discussions, report bugs, suggest ideas
- Open issues, participate on [discussions](https://github.com/mediacms-io/mediacms/discussions), report bugs, suggest ideas
- [Show and tell](https://github.com/mediacms-io/mediacms/discussions/categories/show-and-tell) how you are using the project
- Star the project
- Add functionality, work on a PR, fix an issue!
- Add functionality, work on a PR, fix an issue!
## Contact

View file

@ -59,7 +59,7 @@ def login():
file.writelines(f'USERNAME={json.loads(response.text)["username"]}\n')
print(f"Welcome to MediaCMS [bold blue]{username}[/bold blue]. Your auth creds have been suceesfully stored in the .env file", ":v:")
else:
print(f'Error: {"non_field_errors":["User not found."]}')
print(f'Error: {"non_field_errors": ["User not found."]}')
@apis.command()
@ -73,7 +73,7 @@ def upload_media():
if os.path.isdir(path):
for filename in os.listdir(path):
files = {}
abs = os.path.abspath("{path}/{filename}")
abs = os.path.abspath(f"{path}/{filename}")
files['media_file'] = open(f'{abs}', 'rb')
response = requests.post(url=f'{BASE_URL}/media', headers=headers, files=files)
if response.status_code == 201:

48
cms/dev_settings.py Normal file
View file

@ -0,0 +1,48 @@
# Development settings, used in docker-compose-dev.yaml
import os
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'allauth',
'allauth.account',
'allauth.socialaccount',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
'rest_framework',
'rest_framework.authtoken',
'imagekit',
'files.apps.FilesConfig',
'users.apps.UsersConfig',
'actions.apps.ActionsConfig',
'debug_toolbar',
'mptt',
'crispy_forms',
'uploader.apps.UploaderConfig',
'djcelery_email',
'ckeditor',
'drf_yasg',
'corsheaders',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'debug_toolbar.middleware.DebugToolbarMiddleware',
]
DEBUG = True
CORS_ORIGIN_ALLOW_ALL = True
STATICFILES_DIRS = (os.path.join(BASE_DIR, 'static/'),)
STATIC_ROOT = None

View file

@ -1,7 +1,6 @@
import os
from celery.schedules import crontab
from django.utils.translation import gettext_lazy as _
DEBUG = False
@ -94,6 +93,9 @@ ALLOW_MENTION_IN_COMMENTS = False # allowing to mention other users with @ in t
# valid options: content, author
RELATED_MEDIA_STRATEGY = "content"
# Whether or not to generate a sitemap.xml listing the pages on the site (default: False)
GENERATE_SITEMAP = False
USE_I18N = True
USE_L10N = True
USE_TZ = True
@ -305,7 +307,6 @@ INSTALLED_APPS = [
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.locale.LocaleMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
@ -469,7 +470,7 @@ except ImportError:
if "http" not in FRONTEND_HOST:
# FRONTEND_HOST needs a http:// preffix
FRONTEND_HOST = f"http://{FRONTEND_HOST}"
FRONTEND_HOST = f"http://{FRONTEND_HOST}" # noqa
if LOCAL_INSTALL:
SSL_FRONTEND_HOST = FRONTEND_HOST.replace("http", "https")
@ -488,22 +489,17 @@ if GLOBAL_LOGIN_REQUIRED:
r'/api/v[0-9]+/',
]
# if True, only show original, don't perform any action on videos
DO_NOT_TRANSCODE_VIDEO = False
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
# the following is related to local development using docker
# and docker-compose-dev.yaml
try:
# mostly used in docker-compose-dev.yaml
DEVELOPMENT_MODE = os.environ.get("DEVELOPMENT_MODE")
if DEVELOPMENT_MODE and DEVELOPMENT_MODE == 'True':
if DEVELOPMENT_MODE:
# keep a dev_settings.py file for local overrides
from .dev_settings import * # noqa
except ImportError:
pass
LANGUAGES = [
('el', _('Greek')),
("de", _("German")),
("en", _("English")),
('fr', _('French')),
]
LANGUAGE_CODE = 'en' # default language

View file

@ -13,7 +13,6 @@ schema_view = get_schema_view(
permission_classes=(AllowAny,),
)
# refactor seriously
urlpatterns = [
re_path(r"^__debug__/", include(debug_toolbar.urls)),

View file

@ -28,7 +28,7 @@ else
fi
# We should do this only for folders that have a different owner, since it is an expensive operation
# find /home/mediacms.io/ ! \( -user www-data -group $TARGET_GID \) -exec chown www-data:$TARGET_GID {} +
find /home/mediacms.io/ ! \( -user www-data -group $TARGET_GID \) -exec chown www-data:$TARGET_GID {} +
chmod +x /home/mediacms.io/mediacms/deploy/docker/start.sh /home/mediacms.io/mediacms/deploy/docker/prestart.sh

View file

@ -7,7 +7,7 @@ if [ X"$ENABLE_MIGRATIONS" = X"yes" ]; then
echo "Running migrations service"
python manage.py migrate
EXISTING_INSTALLATION=`echo "from users.models import User; print(User.objects.exists())" |python manage.py shell`
if [ "$EXISTING_INSTALLATION" = "True" ]; then
if [ "$EXISTING_INSTALLATION" = "True" ]; then
echo "Loaddata has already run"
else
echo "Running loaddata and creating admin user"
@ -67,4 +67,5 @@ fi
if [ X"$ENABLE_CELERY_LONG" = X"yes" ] ; then
echo "Enabling celery-long task worker"
cp deploy/docker/supervisord/supervisord-celery_long.conf /etc/supervisor/conf.d/supervisord-celery_long.conf
rm /var/run/mediacms/* -f # remove any stale id, so that on forced restarts of celery workers there are no stale processes that prevent new ones
fi

View file

@ -8,22 +8,9 @@ services:
image: mediacms/mediacms-dev:latest
volumes:
- ./:/home/mediacms.io/mediacms/
command: "python manage.py migrate"
environment:
ENABLE_UWSGI: 'no'
ENABLE_NGINX: 'no'
ENABLE_CELERY_SHORT: 'no'
ENABLE_CELERY_LONG: 'no'
ENABLE_CELERY_BEAT: 'no'
ADMIN_USER: 'admin'
ADMIN_EMAIL: 'admin@localhost'
ADMIN_PASSWORD: 'admin'
FRONTEND_HOST: 'http://localhost'
PORTAL_NAME: 'MediaCMS'
SECRET_KEY: 'ma!s3^b-cw!f#7s6s0m3*jx77a@riw(7701**(r=ww%w!2+yk2'
POSTGRES_HOST: 'db'
DEVELOPMENT_MODE: "True"
REDIS_SSL_CERT_REQS: "none"
command: "./deploy/docker/prestart.sh"
restart: on-failure
depends_on:
redis:
@ -50,14 +37,9 @@ services:
command: "python manage.py runserver 0.0.0.0:80"
environment:
DEVELOPMENT_MODE: "True"
REDIS_SSL_CERT_REQS: "none"
ADMIN_USER: 'admin'
ADMIN_PASSWORD: 'admin'
ADMIN_EMAIL: 'admin@localhost'
FRONTEND_HOST: 'http://localhost'
PORTAL_NAME: 'MediaCMS'
SECRET_KEY: 'ma!s3^b-cw!f#7s6s0m3*jx77a@riw(7701**(r=ww%w!2+yk2'
POSTGRES_HOST: 'db'
ports:
- "80:80"
volumes:
@ -98,11 +80,5 @@ services:
ENABLE_NGINX: 'no'
ENABLE_CELERY_BEAT: 'no'
ENABLE_MIGRATIONS: 'no'
FRONTEND_HOST: 'http://localhost'
PORTAL_NAME: 'MediaCMS'
SECRET_KEY: 'ma!s3^b-cw!f#7s6s0m3*jx77a@riw(7701**(r=ww%w!2+yk2'
POSTGRES_HOST: 'db'
REDIS_SSL_CERT_REQS: "none"
DEVELOPMENT_MODE: "True"
depends_on:
- web

View file

@ -18,7 +18,7 @@
- [15. Debugging email issues](#15-debugging-email-issues)
- [16. Frequently Asked Questions](#16-frequently-asked-questions)
- [17. Cookie consent code](#17-cookie-consent-code)
- [18. Disable encoding and show only original file](#18-disable-encoding-and-show-only-original-file)
## 1. Welcome
This page is created for MediaCMS administrators that are responsible for setting up the software, maintaining it and making modifications.
@ -470,6 +470,14 @@ ADMINS_NOTIFICATIONS = {
- Make the portal workflow public, but at the same time set `GLOBAL_LOGIN_REQUIRED = True` so that only logged in users can see content.
- You can either set `REGISTER_ALLOWED = False` if you want to add members yourself or checkout options on "django-allauth settings" that affects registration in `cms/settings.py`. Eg set the portal invite only, or set email confirmation as mandatory, so that you control who registers.
### 5.24 Enable the sitemap
Whether or not to enable generation of a sitemap file at http://your_installation/sitemap.xml (default: False)
```
GENERATE_SITEMAP = False
```
## 6. Manage pages
to be written
@ -762,3 +770,12 @@ this will re-create the sprites for videos that the task failed.
On file `templates/components/header.html` you can find a simple cookie consent code. It is commented, so you have to remove the `{% comment %}` and `{% endcomment %}` lines in order to enable it. Or you can replace that part with your own code that handles cookie consent banners.
![Simple Cookie Consent](images/cookie_consent.png)
## 18. Disable encoding and show only original file
When videos are uploaded, they are getting encoded to multiple resolutions, a procedure called transcoding. Sometimes this is not needed and you only need to show the original file, eg when MediaCMS is running on a low capabilities server. To achieve this, edit settings.py and set
```
DO_NOT_TRANSCODE_VIDEO = True
```
This will disable the transcoding process and only the original file will be shown. Note that this will also disable the sprites file creation, so you will not have the preview thumbnails on the video player.

60
docs/dev_exp.md Normal file
View file

@ -0,0 +1,60 @@
# Developer Experience
There is ongoing effort to provide a better developer experience and document it.
## How to develop locally with Docker
First install a recent version of [Docker](https://docs.docker.com/get-docker/), and [Docker Compose](https://docs.docker.com/compose/install/).
Then run `docker-compose -f docker-compose-dev.yaml up`
```
user@user:~/mediacms$ docker-compose -f docker-compose-dev.yaml up
```
In a few minutes the app will be available at http://localhost . Login via admin/admin
### What does docker-compose-dev.yaml do?
It build the two images used for backend and frontend.
* Backend: `mediacms/mediacms-dev:latest`
* Frontend: `frontend`
and will start all services required for MediaCMS, as Celery/Redis for asynchronous tasks, PostgreSQL database, Django and React
For Django, the changes from the image produced by docker-compose.yaml are these:
* Django runs in debug mode, with `python manage.py runserver`
* uwsgi and nginx are not run
* Django runs in Debug mode, with Debug Toolbar
* Static files (js/css) are loaded from static/ folder
* corsheaders is installed and configured to allow all origins
For React, it will run `npm start` in the frontend folder, which will start the development server.
Check it on http://localhost:8088/
### How to develop in Django
Django starts at http://localhost and is reloading automatically. Making any change to the python code should refresh Django.
### How to develop in React
React is started on http://localhost:8088/ , code is located in frontend/ , so making changes there should have instant effect on the page. Keep in mind that React is loading data from Django, and that it has to be built so that Django can serve it.
### Making changes to the frontend
The way React is added is more complicated than the usual SPA project and this is because React is used as a library loaded by Django Templates, so it is not a standalone project and is not handling routes etc.
The two directories to consider are:
* frontend/src , for the React files
* templates/, for the Django templates.
Django is using a highly intuitive hierarchical templating system (https://docs.djangoproject.com/en/4.2/ref/templates/), where the base template is templates/root.html and all other templates are extending it.
React is called through the Django templates, eg templates/cms/media.html is loading js/media.js
In order to make changes to React code, edit code on frontend/src and check it's effect on http://localhost:8088/ . Once ready, build it and copy it to the Django static folder, so that it is served by Django.
### Development workflow with the frontend
1. Edit frontend/src/ files
2. Check changes on http://localhost:8088/
3. Build frontend with `docker-compose -f docker-compose-dev.yaml exec frontend npm run dist`
4. Copy static files to Django static folder with`cp -r frontend/dist/static/* static/`
5. Restart Django - `docker-compose -f docker-compose-dev.yaml restart web` so that it uses the new static files
6. Commit the changes

View file

@ -40,6 +40,12 @@ class MediaAdmin(admin.ModelAdmin):
def get_comments_count(self, obj):
return obj.comments.count()
@admin.action(description="Generate missing encoding(s)", permissions=["change"])
def generate_missing_encodings(modeladmin, request, queryset):
for m in queryset:
m.encode(force=False)
actions = [generate_missing_encodings]
get_comments_count.short_description = "Comments count"
@ -74,7 +80,18 @@ class SubtitleAdmin(admin.ModelAdmin):
class EncodingAdmin(admin.ModelAdmin):
pass
list_display = ["get_title", "chunk", "profile", "progress", "status", "has_file"]
list_filter = ["chunk", "profile", "status"]
def get_title(self, obj):
return str(obj)
get_title.short_description = "Encoding"
def has_file(self, obj):
return obj.media_encoding_url is not None
has_file.short_description = "Has file"
admin.site.register(EncodeProfile, EncodeProfileAdmin)

View file

@ -1,6 +1,5 @@
from django.conf import settings
from .frontend_translations import get_frontend_translations
from .methods import is_mediacms_editor, is_mediacms_manager
@ -32,6 +31,4 @@ def stuff(request):
ret["ALLOW_RATINGS_CONFIRMED_EMAIL_ONLY"] = settings.ALLOW_RATINGS_CONFIRMED_EMAIL_ONLY
ret["VIDEO_PLAYER_FEATURED_VIDEO_ON_INDEX_PAGE"] = settings.VIDEO_PLAYER_FEATURED_VIDEO_ON_INDEX_PAGE
ret["RSS_URL"] = "/rss"
ret["FRONTEND_TRANSLATIONS"] = get_frontend_translations(request.LANGUAGE_CODE)
return ret

View file

@ -1,28 +0,0 @@
from django.conf import settings
from .de import translations as de_translations
from .el import translations as el_translations
from .fr import translations as fr_translations
translations = {}
translations['el'] = el_translations
translations['fr'] = fr_translations
translations['de'] = de_translations
def get_frontend_translations(language_code):
if language_code not in [pair[0] for pair in settings.LANGUAGES]:
return {}
if language_code in ['en', 'en-us', 'en-gb']:
return {}
translation = translations[language_code]
# replace any keys from translation that contains a space with an underscore
# do not keep initial keys that contain a space
for key in list(translation.keys()):
if ' ' in key:
translation[key.replace(' ', '_')] = translation.pop(key)
return translation

View file

@ -1,37 +0,0 @@
# German translation for strings in frontend app
translations = {
"Home": "Startseite",
"Featured": "Empfohlen",
"Latest": "Neueste",
"Recommended": "Empfehlungen",
"Recent uploads": "Kürzlich hochgeladen",
"Tags": "Tags",
"Categories": "Kategorien",
"Members": "Mitglieder",
"Upload": "Hochladen",
"Upload media": "Medien hochladen",
"My media": "Meine Medien",
"My playlists": "Meine Wiedergabelisten",
"History": "Verlauf",
"Liked media": "Gefällt mir Medien",
"About": "Über",
"Terms": "Nutzungsbedingungen",
"Contact": "Kontakt",
"Language": "Sprache",
"Manage media": "Medien verwalten",
"Manage users": "Benutzer verwalten",
"Manage comments": "Kommentare verwalten",
"View all": "Alle anzeigen",
"VIEW ALL": "ALLE ANZEIGEN",
"view": "Ansicht",
"views": "Ansichten",
"Search": "Suchen",
"Up next": "Als Nächstes",
"AUTOPLAY": "AUTOPLAY",
"Sign out": "Abmelden",
"Sign in": "Anmelden",
"Register": "Registrieren",
"Edit profile": "Profil bearbeiten",
"Change password": "Passwort ändern",
}

View file

@ -1,37 +0,0 @@
# Greek translation for strings in frontend app
translations = {
"Home": "Αρχική",
"Featured": "Επιλεγμένα",
"Latest": "Πρόσφατα",
"Recommended": "Προτεινόμενα",
"Recent uploads": "Πρόσφατα αρχεία",
"Tags": "Ετικέτες",
"Categories": "Κατηγορίες",
"Members": "Μέλη",
"Upload": "Ανέβασμα αρχείου",
"Upload media": "Ανέβασμα αρχείων",
"My media": "Τα αρχεία μου",
"My playlists": "Οι λίστες μου",
"History": "Ιστορικό",
"Liked media": "Αγαπημένα",
"About": "Σχετικά",
"Terms": "Όροι",
"Contact": "Επικοινωνία",
"Language": "Γλώσσα",
"Manage media": "Διαχείριση αρχείων",
"Manage users": "Διαχείριση χρηστών",
"Manage comments": "Διαχείριση σχολίων",
"View all": "Δές τα όλα",
"VIEW ALL": "ΔΕΣ ΤΑ ΟΛΑ",
"view": "προβολή",
"views": "προβολές",
"Search": "Αναζήτηση",
"Up next": "Επόμενο",
"AUTOPLAY": "Αυτόματη αναπαραγωγή",
"Sign out": "Αποσύνδεση",
"Sign in": "Σύνδεση",
"Register": "Εγγραφή",
"Edit profile": "Επεξεργασία προφιλ",
"Change password": "Αλλαγή κωδικού",
}

View file

@ -1,37 +0,0 @@
# Translations listing
translations = {
"Home": "",
"Featured": "",
"Latest": "",
"Recommended": "",
"Recent uploads": "",
"Tags": "",
"Categories": "",
"Members": "",
"Upload": "",
"Upload media": "",
"My media": "",
"My playlists": "",
"History": "",
"Liked media": "",
"About": "",
"Terms": "",
"Contact": "",
"Language": "",
"Manage media": "",
"Manage users": "",
"Manage comments": "",
"View all": "",
"VIEW ALL": "",
"view": "",
"views": "",
"Search": "",
"Up next": "",
"AUTOPLAY": "",
"Sign out": "",
"Sign in": "",
"Register": "",
"Edit profile": "",
"Change password": "",
}

View file

@ -1,37 +0,0 @@
# French translation for strings in frontend app
translations = {
"Home": "Accueil",
"Featured": "En vedette",
"Latest": "Dernier",
"Recommended": "Recommandé",
"Recent uploads": "Téléchargements récents",
"Tags": "Tags",
"Categories": "Catégories",
"Members": "Membres",
"Upload": "Télécharger",
"Upload media": "Télécharger un média",
"My media": "Mes médias",
"My playlists": "Mes listes de lecture",
"History": "Historique",
"Liked media": "Médias aimés",
"About": "À propos",
"Terms": "Conditions",
"Contact": "Contact",
"Language": "Langue",
"Manage media": "Gérer les médias",
"Manage users": "Gérer les utilisateurs",
"Manage comments": "Gérer les commentaires",
"View all": "Voir tout",
"VIEW ALL": "VOIR TOUT",
"view": "vue",
"views": "vues",
"Search": "Recherche",
"Up next": "À suivre",
"AUTOPLAY": "LECTURE AUTOMATIQUE",
"Sign out": "Se déconnecter",
"Sign in": "Se connecter",
"Register": "S'inscrire",
"Edit profile": "Modifier le profil",
"Change password": "Changer le mot de passe",
}

View file

@ -538,8 +538,8 @@ def get_base_ffmpeg_command(
target_width = round(target_height * 16 / 9)
scale_filter_opts = [
f"if(lt(iw\\,ih)\\,{target_height}\\,{target_width})",
f"if(lt(iw\\,ih)\\,{target_width}\\,{target_height})",
f"if(lt(iw\\,ih)\\,{target_height}\\,{target_width})", # noqa
f"if(lt(iw\\,ih)\\,{target_width}\\,{target_height})", # noqa
"force_original_aspect_ratio=decrease",
"force_divisible_by=2",
"flags=lanczos",

View file

@ -430,8 +430,13 @@ class Media(models.Model):
self.set_media_type()
if self.media_type == "video":
self.set_thumbnail(force=True)
self.produce_sprite_from_video()
self.encode()
if settings.DO_NOT_TRANSCODE_VIDEO:
self.encoding_status = "success"
self.save()
self.produce_sprite_from_video()
else:
self.produce_sprite_from_video()
self.encode()
elif self.media_type == "image":
self.set_thumbnail(force=True)
return True
@ -667,6 +672,13 @@ class Media(models.Model):
return ret
for key in ENCODE_RESOLUTIONS_KEYS:
ret[key] = {}
# if this is enabled, return original file on a way
# that video.js can consume
if settings.DO_NOT_TRANSCODE_VIDEO:
ret['0-original'] = {"h264": {"url": helpers.url_from_path(self.media_file.path), "status": "success", "progress": 100}}
return ret
for encoding in self.encodings.select_related("profile").filter(chunk=False):
if encoding.profile.extension == "gif":
continue

View file

@ -644,7 +644,11 @@ def save_user_action(user_or_session, friendly_token=None, action="watch", extra
if action == "watch":
media.views += 1
media.save(update_fields=["views"])
Media.objects.filter(friendly_token=friendly_token).update(views=media.views)
# update field without calling save, to avoid post_save signals being triggered
# same in other actions
elif action == "report":
media.reported_times += 1
@ -659,10 +663,10 @@ def save_user_action(user_or_session, friendly_token=None, action="watch", extra
)
elif action == "like":
media.likes += 1
media.save(update_fields=["likes"])
Media.objects.filter(friendly_token=friendly_token).update(likes=media.likes)
elif action == "dislike":
media.dislikes += 1
media.save(update_fields=["dislikes"])
Media.objects.filter(friendly_token=friendly_token).update(dislikes=media.dislikes)
return True

View file

@ -7,10 +7,8 @@ from . import management_views, views
from .feeds import IndexRSSFeed, SearchRSSFeed
urlpatterns = [
path("i18n/", include("django.conf.urls.i18n")),
re_path(r"^$", views.index),
re_path(r"^about", views.about, name="about"),
re_path(r"^setlanguage", views.setlanguage, name="setlanguage"),
re_path(r"^add_subtitle", views.add_subtitle, name="add_subtitle"),
re_path(r"^categories$", views.categories, name="categories"),
re_path(r"^contact$", views.contact, name="contact"),
@ -91,3 +89,6 @@ urlpatterns = [
re_path(r"^manage/media$", views.manage_media, name="manage_media"),
re_path(r"^manage/users$", views.manage_users, name="manage_users"),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
if hasattr(settings, "GENERATE_SITEMAP") and settings.GENERATE_SITEMAP:
urlpatterns.append(path("sitemap.xml", views.sitemap, name="sitemap"))

View file

@ -74,13 +74,6 @@ def about(request):
return render(request, "cms/about.html", context)
def setlanguage(request):
"""Set Language view"""
context = {}
return render(request, "cms/set_language.html", context)
@login_required
def add_subtitle(request):
"""Add subtitle view"""
@ -294,11 +287,21 @@ def search(request):
"""Search view"""
context = {}
RSS_URL = f"/rss{request.environ.get('REQUEST_URI')}"
RSS_URL = f"/rss{request.environ['REQUEST_URI']}"
context["RSS_URL"] = RSS_URL
return render(request, "cms/search.html", context)
def sitemap(request):
"""Sitemap"""
context = {}
context["media"] = list(Media.objects.filter(Q(listable=True)).order_by("-add_date"))
context["playlists"] = list(Playlist.objects.filter().order_by("-add_date"))
context["users"] = list(User.objects.filter())
return render(request, "sitemap.xml", context, content_type="application/xml")
def tags(request):
"""List tags view"""

View file

@ -1,5 +1,4 @@
import React from 'react';
import { translate_string } from '../utils/helpers/';
interface MediaListHeaderProps {
title?: string;
@ -10,7 +9,7 @@ interface MediaListHeaderProps {
}
export const MediaListHeader: React.FC<MediaListHeaderProps> = (props) => {
const viewAllText = props.viewAllText || translate_string('VIEW ALL');
const viewAllText = props.viewAllText || 'VIEW ALL';
return (
<div className={(props.className ? props.className + ' ' : '') + 'media-list-header'} style={props.style}>
<h2>{props.title}</h2>

View file

@ -2,7 +2,6 @@ import React from 'react';
import { format } from 'timeago.js';
import { formatViewsNumber, imageExtension } from '../../../../utils/helpers/';
import { VideoPlayerByPageLink } from '../../../video-player/VideoPlayerByPageLink';
import { translate_string } from '../../../../utils/helpers/';
export function ItemDescription(props) {
return '' === props.description ? null : (
@ -136,7 +135,7 @@ export function MediaItemAuthorLink(props) {
export function MediaItemMetaViews(props) {
return (
<span className="item-views">{formatViewsNumber(props.views) + ' ' + (1 >= props.views ? translate_string('view') : translate_string('views'))}</span>
<span className="item-views">{formatViewsNumber(props.views) + ' ' + (1 >= props.views ? 'view' : 'views')}</span>
);
}

View file

@ -22,7 +22,7 @@ function downloadOptions(mediaData, allowDownload) {
if (Object.keys(encodingsInfo[k]).length) {
for (g in encodingsInfo[k]) {
if (encodingsInfo[k].hasOwnProperty(g)) {
if ('success' === encodingsInfo[k][g].status && 100 === encodingsInfo[k][g].progress) {
if ('success' === encodingsInfo[k][g].status && 100 === encodingsInfo[k][g].progress && null !== encodingsInfo[k][g].url) {
options[encodingsInfo[k][g].title] = {
text: k + ' - ' + g.toUpperCase() + ' (' + encodingsInfo[k][g].size + ')',
link: formatInnerLink(encodingsInfo[k][g].url, site.url),

View file

@ -19,7 +19,7 @@ function downloadOptionsList() {
if (Object.keys(encodings_info[k]).length) {
for (g in encodings_info[k]) {
if (encodings_info[k].hasOwnProperty(g)) {
if ('success' === encodings_info[k][g].status && 100 === encodings_info[k][g].progress) {
if ('success' === encodings_info[k][g].status && 100 === encodings_info[k][g].progress && null !== encodings_info[k][g].url) {
optionsList[encodings_info[k][g].title] = {
text: k + ' - ' + g.toUpperCase() + ' (' + encodings_info[k][g].size + ')',
link: formatInnerLink(encodings_info[k][g].url, SiteContext._currentValue.url),

View file

@ -2,7 +2,6 @@ import React, { useState, useEffect } from 'react';
import { PageActions } from '../../utils/actions/';
import { PageStore, MediaPageStore } from '../../utils/stores/';
import { ItemList } from '../item-list/ItemList';
import { translate_string } from '../../utils/helpers/';
function autoPlayMedia() {
const dt = MediaPageStore.get('media-data');
@ -39,10 +38,10 @@ export function AutoPlay(props) {
return !media ? null : (
<div className="auto-play">
<div className="auto-play-header">
<div className="next-label">{translate_string("Up next")}</div>
<div className="next-label">Up next</div>
<div className="auto-play-option">
<label className="checkbox-label right-selectbox" tabIndex={0} onKeyPress={onKeyPress}>
{translate_string("AUTOPLAY")}
AUTOPLAY
<span className="checkbox-switcher-wrap">
<span className="checkbox-switcher">
<input

View file

@ -4,7 +4,6 @@ import { PageStore } from '../../../utils/stores/';
import { HeaderConsumer, MemberConsumer, LinksConsumer } from '../../../utils/contexts/';
import { CircleIconButton, MaterialIcon, NavigationContentApp, NavigationMenuList, PopupTop, PopupMain, UserThumbnail } from '../../_shared';
import { HeaderThemeSwitcher } from './HeaderThemeSwitcher';
import { translate_string } from '../../../utils/helpers/';
function headerPopupPages(user, popupNavItems, hasHeaderThemeSwitcher) {
const pages = {
@ -96,9 +95,9 @@ function LoginButton({ user, link, hasHeaderThemeSwitcher }) {
className={
'button-link sign-in' + (hasHeaderThemeSwitcher ? ' hidden-only-in-small' : ' hidden-only-in-extra-small')
}
title={translate_string('Sign in')}
title="Sign in"
>
{translate_string('Sign in')}
Sign in
</a>
</div>
) : null;
@ -113,9 +112,9 @@ function RegisterButton({ user, link, hasHeaderThemeSwitcher }) {
'button-link register-link' +
(hasHeaderThemeSwitcher ? ' hidden-only-in-small' : ' hidden-only-in-extra-small')
}
title={translate_string('Register')}
title="Register"
>
{translate_string('Register')}
Register
</a>
</div>
) : null;

View file

@ -4,7 +4,6 @@ import { LinksContext } from '../../../utils/contexts/';
import { PageStore, SearchFieldStore } from '../../../utils/stores/';
import { SearchFieldActions } from '../../../utils/actions/';
import { MaterialIcon, PopupMain } from '../../_shared';
import { translate_string } from '../../../utils/helpers/';
import './SearchField.scss';
@ -297,7 +296,7 @@ export function SearchField(props) {
<input
ref={searchInputRef}
type="text"
placeholder={translate_string("Search")}
placeholder="Search"
aria-label="Search"
name="q"
value={queryVal}

View file

@ -4,7 +4,6 @@ import { useUser } from '../../../utils/hooks/';
import { PageStore } from '../../../utils/stores/';
import { LinksContext, SidebarContext } from '../../../utils/contexts/';
import { NavigationMenuList } from '../../_shared';
import { translate_string } from '../../../utils/helpers/';
export function SidebarNavigationMenu() {
const { userCan, isAnonymous, pages: userPages } = useUser();
@ -41,7 +40,7 @@ export function SidebarNavigationMenu() {
items.push({
link: links.home,
icon: 'home',
text: translate_string('Home'),
text: 'Home',
className: 'nav-item-home',
});
}
@ -50,7 +49,7 @@ export function SidebarNavigationMenu() {
items.push({
link: links.featured,
icon: 'star',
text: translate_string('Featured'),
text: PageStore.get('config-enabled').pages.featured.title,
className: 'nav-item-featured',
});
}
@ -62,7 +61,7 @@ export function SidebarNavigationMenu() {
items.push({
link: links.recommended,
icon: 'done_outline',
text: translate_string("Recommended"),
text: PageStore.get('config-enabled').pages.recommended.title,
className: 'nav-item-recommended',
});
}
@ -71,7 +70,7 @@ export function SidebarNavigationMenu() {
items.push({
link: links.latest,
icon: 'new_releases',
text: translate_string("Latest"),
text: PageStore.get('config-enabled').pages.latest.title,
className: 'nav-item-latest',
});
}
@ -84,7 +83,7 @@ export function SidebarNavigationMenu() {
items.push({
link: links.archive.tags,
icon: 'local_offer',
text: translate_string("Tags"),
text: PageStore.get('config-enabled').taxonomies.tags.title,
className: 'nav-item-tags',
});
}
@ -97,7 +96,7 @@ export function SidebarNavigationMenu() {
items.push({
link: links.archive.categories,
icon: 'list_alt',
text: translate_string("Categories"),
text: PageStore.get('config-enabled').taxonomies.categories.title,
className: 'nav-item-categories',
});
}
@ -106,7 +105,7 @@ export function SidebarNavigationMenu() {
items.push({
link: links.members,
icon: 'people',
text: translate_string("Members"),
text: PageStore.get('config-enabled').pages.members.title,
className: 'nav-item-members',
});
}
@ -133,7 +132,7 @@ export function SidebarNavigationMenu() {
items.push({
link: links.user.addMedia,
icon: 'video_call',
text: translate_string("Upload"),
text: 'Upload media',
className: 'nav-item-upload-media',
});
@ -141,7 +140,7 @@ export function SidebarNavigationMenu() {
items.push({
link: userPages.media,
icon: 'video_library',
text: translate_string("My media"),
text: 'My media',
className: 'nav-item-my-media',
});
}
@ -151,7 +150,7 @@ export function SidebarNavigationMenu() {
items.push({
link: userPages.playlists,
icon: 'playlist_play',
text: translate_string("My playlists"),
text: 'My playlists',
className: 'nav-item-my-playlists',
});
}
@ -167,7 +166,7 @@ export function SidebarNavigationMenu() {
items.push({
link: links.user.history,
icon: 'history',
text: translate_string("History"),
text: PageStore.get('config-enabled').pages.history.title,
className: 'nav-item-history',
});
}
@ -180,7 +179,7 @@ export function SidebarNavigationMenu() {
items.push({
link: links.user.liked,
icon: 'thumb_up',
text: translate_string("Liked media"),
text: PageStore.get('config-enabled').pages.liked.title,
className: 'nav-item-liked',
});
}
@ -189,35 +188,7 @@ export function SidebarNavigationMenu() {
}
function CustomMenuSection() {
const items = [];
items.push({
link: '/about',
icon: 'contact_support',
text: translate_string("About"),
className: 'nav-item-about',
});
items.push({
link: '/tos',
icon: 'description',
text: translate_string("Terms"),
className: 'nav-item-terms',
});
items.push({
link: '/contact',
icon: 'alternate_email',
text: translate_string("Contact"),
className: 'nav-item-contact',
});
items.push({
link: '/setlanguage',
icon: 'language',
text: translate_string("Language"),
className: 'nav-item-language',
});
const items = PageStore.get('config-contents').sidebar.navMenu.items;
return items.length ? <NavigationMenuList key="custom" items={formatItems(items)} /> : null;
}
@ -229,7 +200,7 @@ export function SidebarNavigationMenu() {
items.push({
link: links.manage.media,
icon: 'miscellaneous_services',
text: translate_string("Manage media"),
text: 'Manage media',
className: 'nav-item-manage-media',
});
}
@ -238,7 +209,7 @@ export function SidebarNavigationMenu() {
items.push({
link: links.manage.users,
icon: 'miscellaneous_services',
text: translate_string("Manage users"),
text: 'Manage users',
className: 'nav-item-manage-users',
});
}
@ -247,7 +218,7 @@ export function SidebarNavigationMenu() {
items.push({
link: links.manage.comments,
icon: 'miscellaneous_services',
text: translate_string("Manage comments"),
text: 'Manage comments',
className: 'nav-item-manage-comments',
});
}

View file

@ -3,14 +3,13 @@ import { ApiUrlConsumer } from '../utils/contexts/';
import { MediaListWrapper } from '../components/MediaListWrapper';
import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListAsync.jsx';
import { Page } from './Page';
import { translate_string } from '../utils/helpers/';
interface CategoriesPageProps {
id?: string;
title?: string;
}
export const CategoriesPage: React.FC<CategoriesPageProps> = ({ id = 'categories', title = translate_string('Categories') }) => (
export const CategoriesPage: React.FC<CategoriesPageProps> = ({ id = 'categories', title = 'Categories' }) => (
<Page id={id}>
<ApiUrlConsumer>
{(apiUrl) => (

View file

@ -4,7 +4,6 @@ import { PageStore } from '../utils/stores/';
import { MediaListWrapper } from '../components/MediaListWrapper';
import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListAsync.jsx';
import { Page } from './Page';
import { translate_string } from '../utils/helpers/';
interface FeaturedMediaPageProps {
id?: string;
@ -13,7 +12,7 @@ interface FeaturedMediaPageProps {
export const FeaturedMediaPage: React.FC<FeaturedMediaPageProps> = ({
id = 'featured-media',
title = translate_string('Featured'),
title = PageStore.get('config-enabled').pages.featured.title,
}) => (
<Page id={id}>
<ApiUrlConsumer>

View file

@ -7,7 +7,6 @@ import { MediaListWrapper } from '../components/MediaListWrapper';
import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListAsync.jsx';
import { ProfileHistoryPage } from './ProfileHistoryPage';
import { Page } from './Page';
import { translate_string } from '../utils/helpers/';
declare global {
interface Window {
@ -22,7 +21,7 @@ interface AnonymousHistoryPageProps {
export const AnonymousHistoryPage: React.FC<AnonymousHistoryPageProps> = ({
id = 'history-media',
title = translate_string('History'),
title = PageStore.get('config-enabled').pages.history.title,
}) => {
const [resultsCount, setResultsCount] = useState<number | null>(null);

View file

@ -6,7 +6,6 @@ import { MediaMultiListWrapper } from '../components/MediaMultiListWrapper';
import { ItemListAsync } from '../components/item-list/ItemListAsync.jsx';
import { InlineSliderItemListAsync } from '../components/item-list/InlineSliderItemListAsync.jsx';
import { Page } from './Page';
import { translate_string } from '../utils/helpers/';
const EmptyMedia: React.FC = ({}) => {
return (
@ -36,12 +35,9 @@ interface HomePageProps {
export const HomePage: React.FC<HomePageProps> = ({
id = 'home',
//featured_title = PageStore.get('config-options').pages.home.sections.featured.title,
//recommended_title = PageStore.get('config-options').pages.home.sections.recommended.title,
//latest_title = PageStore.get('config-options').pages.home.sections.latest.title,
featured_title = translate_string('Featured'),
recommended_title = translate_string('Recommended'),
latest_title = translate_string('Latest'),
featured_title = PageStore.get('config-options').pages.home.sections.featured.title,
recommended_title = PageStore.get('config-options').pages.home.sections.recommended.title,
latest_title = PageStore.get('config-options').pages.home.sections.latest.title,
latest_view_all_link = false,
featured_view_all_link = true,
recommended_view_all_link = true,

View file

@ -4,7 +4,6 @@ import { PageStore } from '../utils/stores/';
import { MediaListWrapper } from '../components/MediaListWrapper';
import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListAsync';
import { Page } from './Page';
import { translate_string } from '../utils/helpers/';
interface LatestMediaPageProps {
id?: string;
@ -13,7 +12,7 @@ interface LatestMediaPageProps {
export const LatestMediaPage: React.FC<LatestMediaPageProps> = ({
id = 'latest-media',
title = translate_string('Recent uploads'),
title = PageStore.get('config-enabled').pages.latest.title,
}) => (
<Page id={id}>
<ApiUrlConsumer>

View file

@ -7,7 +7,6 @@ import { MediaListWrapper } from '../components/MediaListWrapper';
import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListAsync';
import { ProfileLikedPage } from './ProfileLikedPage';
import { Page } from './Page';
import { translate_string } from '../utils/helpers/';
declare global {
interface Window {
@ -22,7 +21,7 @@ interface AnonymousLikedMediaPageProps {
export const AnonymousLikedMediaPage: React.FC<AnonymousLikedMediaPageProps> = ({
id = 'liked-media',
title = translate_string('Liked media'),
title = PageStore.get('config-enabled').pages.liked.title,
}) => {
const [resultsCount, setResultsCount] = useState<number | null>(null);

View file

@ -4,7 +4,6 @@ import { PageStore } from '../utils/stores/';
import { MediaListWrapper } from '../components/MediaListWrapper';
import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListAsync.jsx';
import { Page } from './Page';
import { translate_string } from '../utils/helpers/';
interface RecommendedMediaPageProps {
id?: string;
@ -13,7 +12,7 @@ interface RecommendedMediaPageProps {
export const RecommendedMediaPage: React.FC<RecommendedMediaPageProps> = ({
id = 'recommended-media',
title = translate_string('Recommended'),
title = PageStore.get('config-enabled').pages.recommended.title,
}) => (
<Page id={id}>
<ApiUrlConsumer>

View file

@ -3,14 +3,13 @@ import { ApiUrlConsumer } from '../utils/contexts/';
import { MediaListWrapper } from '../components/MediaListWrapper';
import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListAsync.jsx';
import { Page } from './Page';
import { translate_string } from '../utils/helpers/';
interface TagsPageProps {
id?: string;
title?: string;
}
export const TagsPage: React.FC<TagsPageProps> = ({ id = 'tags', title = translate_string('Tags') }) => (
export const TagsPage: React.FC<TagsPageProps> = ({ id = 'tags', title = 'Tags' }) => (
<Page id={id}>
<ApiUrlConsumer>
{(apiUrl) => (

View file

@ -1,5 +1,4 @@
import { addClassname, removeClassname } from '../helpers/';
import { translate_string } from '../../utils/helpers/';
export function UpNextLoaderView(nextItemData) {
var timerTimeout;
@ -40,7 +39,7 @@ export function UpNextLoaderView(nextItemData) {
domElems.nextMediaTitle.setAttribute('class', 'next-media-title');
domElems.nextMediaAuthor.setAttribute('class', 'next-media-author');
domElems.upNextLabel.innerHTML = translate_string('Up Next');
domElems.upNextLabel.innerHTML = 'Up Next';
domElems.nextMediaTitle.innerHTML = nextItemData.title;
domElems.nextMediaAuthor.innerHTML = nextItemData.author_name;

View file

@ -1,6 +1,5 @@
import React, { createContext } from 'react';
import { config as mediacmsConfig } from '../settings/config.js';
import { translate_string } from '../../utils/helpers/';
const config = mediacmsConfig(window.MediaCMS);
@ -18,7 +17,7 @@ function popupTopNavItems() {
items.push({
link: links.user.addMedia,
icon: 'video_call',
text: translate_string('Upload media'),
text: 'Upload media',
itemAttr: {
className: 'visible-only-in-small',
},
@ -28,7 +27,7 @@ function popupTopNavItems() {
items.push({
link: user.pages.media,
icon: 'video_library',
text: translate_string('My media'),
text: 'My media',
});
}
}
@ -36,7 +35,7 @@ function popupTopNavItems() {
items.push({
link: links.signout,
icon: 'exit_to_app',
text: translate_string('Sign out'),
text: 'Sign out',
});
}
@ -65,7 +64,7 @@ function popupMiddleNavItems() {
itemType: 'link',
icon: 'login',
iconPos: 'left',
text: translate_string('Sign in'),
text: 'Sign in',
link: links.signin,
linkAttr: {
className: hasThemeSwitcher ? 'visible-only-in-small' : 'visible-only-in-extra-small',
@ -78,7 +77,7 @@ function popupMiddleNavItems() {
itemType: 'link',
icon: 'person_add',
iconPos: 'left',
text: translate_string('Register'),
text: 'Register',
link: links.register,
linkAttr: {
className: hasThemeSwitcher ? 'visible-only-in-small' : 'visible-only-in-extra-small',
@ -89,14 +88,14 @@ function popupMiddleNavItems() {
items.push({
link: links.user.editProfile,
icon: 'brush',
text: translate_string('Edit profile'),
text: 'Edit profile',
});
if (user.can.changePassword) {
items.push({
link: links.changePassword,
icon: 'lock',
text: translate_string('Change password'),
text: 'Change password',
});
}
}

View file

@ -12,4 +12,3 @@ export * from './propTypeFilters';
export { default as publishedOnDate } from './publishedOnDate';
export * from './quickSort';
export * from './requests';
export { translate_string } from './translate';

View file

@ -1,9 +0,0 @@
// check templates/config/installation/translations.html for more
export function translate_string(string) {
if (window.TRANSLATIONS && window.TRANSLATIONS[string]) {
return window.TRANSLATIONS[string];
} else {
return string;
}
}

View file

@ -12,3 +12,5 @@ pytest-cov
pytest-django
pytest-factoryboy
Faker
django-cors-headers

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,65 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% block headtitle %}About - {{PORTAL_NAME}}{% endblock headtitle %}
{% block headermeta %}
<meta property="og:title" content="About - {{PORTAL_NAME}}">
<meta property="og:type" content="website">
<meta property="og:description" content="">
<meta name="twitter:card" content="summary">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [{
"@type": "ListItem",
"position": 1,
"name": "{{PORTAL_NAME}}",
"item": {
"@type": "WebPage",
"@id": "{{FRONTEND_HOST}}"
}
},
{
"@type": "ListItem",
"position": 2,
"name": "About",
"item": {
"@type": "AboutPage",
"@id": "{{FRONTEND_HOST}}/about"
}
}]
}
</script>
{% endblock headermeta %}
{% block innercontent %}
<div class="custom-page-wrapper">
<h1>Change Language</h1>
<hr/>
<h2>Select</h2>
<p>
<form action="{% url 'set_language' %}" method="post">{% csrf_token %}
<input name="next" type="hidden" value="/">
<select name="language">
{% get_current_language as LANGUAGE_CODE %}
{% get_available_languages as LANGUAGES %}
{% get_language_info_list for LANGUAGES as languages %}
{% for language in languages %}
<option value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected{% endif %}>
{{ language.name_local }} ({{ language.code }})
</option>
{% endfor %}
</select>
<input type="submit" value="Go">
</form>
</p>
</div>
{% endblock %}

View file

@ -1,11 +1,10 @@
<script type="text/javascript">
var MediaCMS = {
{% if media %}mediaId: "{{media}}", {% endif %}
{% if user %}profileId: "{{user.username}}", {% endif %}
};
{% include "config/core/api.html" %}
{% include "config/core/url.html" %}
{% include "config/core/user.html" %}
@ -15,8 +14,6 @@
{% include "config/installation/pages.html" %}
{% include "config/installation/site.html" %}
{% include "config/installation/translations.html" %}
window.MediaCMS = MediaCMS;
</script>

View file

@ -1,57 +0,0 @@
// These are the translations that will be used in frontend/src using the helper function
// translate_string (frontend/src/static/js/utils/helpers/translate.js)
// They are fetched from the backend, and specifically files/frontend_translations
// The Django backend is passing dictionary FRONTEND_TRANSLATIONS.
// This contains key values where keys do not have a space
// TRANSLATIONS object will be availaible as window.TRANSLATIONS
// IMPORTANT: If you are starting the frontend app (localhost:8088), you are not getting at this
// POINT, so there are not translations. Translations can only be available and tested on the Django app
// and templates, that are loading React as well
// TODO: pass the python dictionary on a way that can be parsed here and thus no need
// for this duplication taking place here
var TRANSLATIONS = {
"Home": "{{FRONTEND_TRANSLATIONS.Home}}",
"Featured": "{{FRONTEND_TRANSLATIONS.Featured}}",
"View all": "{{FRONTEND_TRANSLATIONS.View_all}}",
"VIEW ALL": "{{FRONTEND_TRANSLATIONS.VIEW_ALL}}",
"Latest": "{{FRONTEND_TRANSLATIONS.Latest}}",
"Recommended": "{{FRONTEND_TRANSLATIONS.Recommended}}",
"Recent uploads": "{{FRONTEND_TRANSLATIONS.Recent_uploads}}",
"view": "{{FRONTEND_TRANSLATIONS.view}}",
"views": "{{FRONTEND_TRANSLATIONS.views}}",
"Search": "{{FRONTEND_TRANSLATIONS.Search}}",
"Tags": "{{FRONTEND_TRANSLATIONS.Tags}}",
"Categories": "{{FRONTEND_TRANSLATIONS.Categories}}",
"Members": "{{FRONTEND_TRANSLATIONS.Members}}",
"Upload": "{{FRONTEND_TRANSLATIONS.Upload}}",
"Upload media": "{{FRONTEND_TRANSLATIONS.Upload_media}}",
"My media": "{{FRONTEND_TRANSLATIONS.My_media}}",
"My playlists": "{{FRONTEND_TRANSLATIONS.My_playlists}}",
"History": "{{FRONTEND_TRANSLATIONS.History}}",
"Liked media": "{{FRONTEND_TRANSLATIONS.Liked_media}}",
"About": "{{FRONTEND_TRANSLATIONS.About}}",
"Terms": "{{FRONTEND_TRANSLATIONS.Terms}}",
"Contact": "{{FRONTEND_TRANSLATIONS.Contact}}",
"Language": "{{FRONTEND_TRANSLATIONS.Language}}",
"Manage media": "{{FRONTEND_TRANSLATIONS.Manage_media}}",
"Manage users": "{{FRONTEND_TRANSLATIONS.Manage_users}}",
"Manage comments": "{{FRONTEND_TRANSLATIONS.Manage_comments}}",
"Up next": "{{FRONTEND_TRANSLATIONS.Up_next}}",
"AUTOPLAY": "{{FRONTEND_TRANSLATIONS.AUTOPLAY}}",
"Sign out": "{{FRONTEND_TRANSLATIONS.Sign_out}}",
"Sign in": "{{FRONTEND_TRANSLATIONS.Sign_in}}",
"Register": "{{FRONTEND_TRANSLATIONS.Register}}",
"Edit profile": "{{FRONTEND_TRANSLATIONS.Edit_profile}}",
"Change password": "{{FRONTEND_TRANSLATIONS.Change_password}}",
}
window.TRANSLATIONS = TRANSLATIONS;

View file

@ -1,2 +1,3 @@
User-Agent: *
Allow: /
Sitemap: {{ FRONTEND_HOST }}/sitemap.xml

66
templates/sitemap.xml Normal file
View file

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
{% load static %}
<url>
<loc>{{ FRONTEND_HOST }}</loc>
<changefreq>always</changefreq>
</url>
<url>
<loc>{{ FRONTEND_HOST }}/featured</loc>
<changefreq>daily</changefreq>
</url>
<url>
<loc>{{ FRONTEND_HOST }}/recommended</loc>
<changefreq>always</changefreq>
</url>
<url>
<loc>{{ FRONTEND_HOST }}/latest</loc>
<changefreq>hourly</changefreq>
</url>
<url>
<loc>{{ FRONTEND_HOST }}/members</loc>
<changefreq>daily</changefreq>
</url>
<url>
<loc>{{ FRONTEND_HOST }}/tags</loc>
<changefreq>daily</changefreq>
</url>
<url>
<loc>{{ FRONTEND_HOST }}/categories</loc>
<changefreq>weekly</changefreq>
</url>
<url>
<loc>{{ FRONTEND_HOST }}/history</loc>
<changefreq>always</changefreq>
</url>
<url>
<loc>{{ FRONTEND_HOST }}/liked</loc>
</url>
<url>
<loc>{{ FRONTEND_HOST }}/about</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>{{ FRONTEND_HOST }}/tos</loc>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>{{ FRONTEND_HOST }}/contact</loc>
<changefreq>never</changefreq>
</url>
{% for media_object in media %}
<url>
<loc>{{ FRONTEND_HOST}}/view?m={{ media_object.friendly_token }}</loc>
</url>
{% endfor %}
{% for playlist_object in playlists %}
<url>
<loc>{{ FRONTEND_HOST}}/playlists/{{ playlist_object.friendly_token }}</loc>
</url>
{% endfor %}
{% for user_object in users %}
<url>
<loc>{{ FRONTEND_HOST}}/user/{{ user_object.username }}/</loc>
</url>
{% endfor %}
</urlset>