mirror of
https://github.com/chibicitiberiu/ytsm.git
synced 2024-02-24 05:43:31 +00:00
Big refactor
This commit is contained in:
1 parent
291da16461
commit
ae77251883
42 files changed
+4311
-264
No files matched your search
@@ -1,5 +1,6 @@
|
|||||||
.vs
|
.vs
|
||||||
.vscode
|
.vscode
|
||||||
|
temp/
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="JavaScriptLibraryMappings">
|
||||||
|
<file url="file://$PROJECT_DIR$" libraries="{bootstrap, jquery-3.3.1, popper}" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+586
-247
File diff suppressed because it is too large.
Load diff
Generated
+3
@@ -16,6 +16,9 @@
|
|||||||
<content url="file://$MODULE_DIR$" />
|
<content url="file://$MODULE_DIR$" />
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
<orderEntry type="library" name="jquery-3.3.1" level="application" />
|
||||||
|
<orderEntry type="library" name="popper" level="application" />
|
||||||
|
<orderEntry type="library" name="bootstrap" level="application" />
|
||||||
</component>
|
</component>
|
||||||
<component name="TemplatesService">
|
<component name="TemplatesService">
|
||||||
<option name="TEMPLATE_CONFIGURATION" value="Django" />
|
<option name="TEMPLATE_CONFIGURATION" value="Django" />
|
||||||
|
|||||||
+20
-1
@@ -15,7 +15,6 @@ import os
|
|||||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
# Quick-start development settings - unsuitable for production
|
||||||
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
|
||||||
|
|
||||||
@@ -63,6 +62,7 @@ TEMPLATES = [
|
|||||||
'context_processors': [
|
'context_processors': [
|
||||||
'django.template.context_processors.debug',
|
'django.template.context_processors.debug',
|
||||||
'django.template.context_processors.request',
|
'django.template.context_processors.request',
|
||||||
|
'django.template.context_processors.media',
|
||||||
'django.contrib.auth.context_processors.auth',
|
'django.contrib.auth.context_processors.auth',
|
||||||
'django.contrib.messages.context_processors.messages',
|
'django.contrib.messages.context_processors.messages',
|
||||||
],
|
],
|
||||||
@@ -121,3 +121,22 @@ USE_TZ = True
|
|||||||
# https://docs.djangoproject.com/en/1.11/howto/static-files/
|
# https://docs.djangoproject.com/en/1.11/howto/static-files/
|
||||||
|
|
||||||
STATIC_URL = '/static/'
|
STATIC_URL = '/static/'
|
||||||
|
MEDIA_URL = '/media/'
|
||||||
|
MEDIA_ROOT = 'D:\\Dev\\youtube-channel-manager\\temp\\media'
|
||||||
|
|
||||||
|
|
||||||
|
# Application settings
|
||||||
|
# These should be moved to an ini file or something
|
||||||
|
DOWNLOADER_PATH = "D:\\Dev\\youtube-channel-manager\\temp\\download"
|
||||||
|
DOWNLOADER_FILE_PATTERN = "{channel}/{playlist}/S01E{playlist_index} - {title} [{id}].{ext}"
|
||||||
|
DOWNLOADER_FORMAT = "bestvideo+bestaudio"
|
||||||
|
DOWNLOADER_SUBTITLES = True
|
||||||
|
DOWNLOADER_SUBTITLES_LANGS = None
|
||||||
|
DOWNLOADER_AUTOGENERATED_SUBTITLES = False
|
||||||
|
DOWNLOADER_DEFAULT_DOWNLOAD_ENABLED = True
|
||||||
|
DOWNLOADER_DEFAULT_DOWNLOAD_LIMIT = 5 # -1 = no limit (all)
|
||||||
|
DOWNLOADER_DEFAULT_ORDER = 'playlist_index' # publish_date, playlist_index
|
||||||
|
MANAGER_MARK_DELETED_AS_WATCHED = True
|
||||||
|
MANAGER_DEFAULT_DELETE_AFTER_WATCHED = True
|
||||||
|
|
||||||
|
|
||||||
+4
-1
@@ -15,6 +15,8 @@ Including another URLconf
|
|||||||
"""
|
"""
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.conf.urls.static import static
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
from YtManagerApp import views
|
from YtManagerApp import views
|
||||||
|
|
||||||
@@ -26,5 +28,6 @@ urlpatterns = [
|
|||||||
path('ajax/delete_folder/<int:fid>/', views.ajax_delete_folder, name='ajax_delete_folder'),
|
path('ajax/delete_folder/<int:fid>/', views.ajax_delete_folder, name='ajax_delete_folder'),
|
||||||
path('ajax/edit_subscription', views.ajax_edit_subscription, name='ajax_edit_subscription'),
|
path('ajax/edit_subscription', views.ajax_edit_subscription, name='ajax_edit_subscription'),
|
||||||
path('ajax/delete_subscription/<int:sid>/', views.ajax_delete_subscription, name='ajax_delete_subscription'),
|
path('ajax/delete_subscription/<int:sid>/', views.ajax_delete_subscription, name='ajax_delete_subscription'),
|
||||||
|
path('ajax/list_videos', views.ajax_list_videos, name='ajax_list_videos'),
|
||||||
path(r'', views.index, name='home')
|
path(r'', views.index, name='home')
|
||||||
]
|
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
from shutil import copyfile
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
from .models import UserSettings
|
||||||
|
from .utils.customconfigparser import ConfigParserWithEnv
|
||||||
|
|
||||||
|
|
||||||
|
class AppConfig(object):
|
||||||
|
__SETTINGS_FILE = 'config.ini'
|
||||||
|
__LOG_FILE = 'log.log'
|
||||||
|
|
||||||
|
DEFAULT_SETTINGS = {
|
||||||
|
'global': {
|
||||||
|
'YouTubeApiKey': 'AIzaSyBabzE4Bup77WexdLMa9rN9z-wJidEfNX8',
|
||||||
|
'SynchronizationSchedule': '0 * * * * *',
|
||||||
|
'SchedulerConcurrency': '2',
|
||||||
|
},
|
||||||
|
'user': {
|
||||||
|
'MarkDeletedAsWatched': 'True',
|
||||||
|
'DeleteWatched': 'True',
|
||||||
|
'AutoDownload': 'True',
|
||||||
|
'DownloadMaxAttempts': '3',
|
||||||
|
'DownloadGlobalLimit': '',
|
||||||
|
'DownloadSubscriptionLimit': '5',
|
||||||
|
'DownloadOrder': 'playlist_index',
|
||||||
|
'DownloadPath': '${env:USERPROFILE}${env:HOME}/Downloads',
|
||||||
|
'DownloadFilePattern': '${channel}/${playlist}/S01E${playlist_index} - ${title} [${id}]',
|
||||||
|
'DownloadFormat': 'bestvideo+bestaudio',
|
||||||
|
'DownloadSubtitles': 'True',
|
||||||
|
'DownloadAutogeneratedSubtitles': 'False',
|
||||||
|
'DownloadSubtitlesAll': 'False',
|
||||||
|
'DownloadSubtitlesLangs': 'en,ro',
|
||||||
|
'DownloadSubtitlesFormat': '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.log_path = os.path.join(settings.BASE_DIR, 'config', AppConfig.__LOG_FILE)
|
||||||
|
self.settings_path = os.path.join(settings.BASE_DIR, 'config', AppConfig.__SETTINGS_FILE)
|
||||||
|
|
||||||
|
self.settings = ConfigParserWithEnv(defaults=AppConfig.DEFAULT_SETTINGS, allow_no_value=True)
|
||||||
|
self.load_settings()
|
||||||
|
|
||||||
|
def load_settings(self):
|
||||||
|
if os.path.exists(self.settings_path):
|
||||||
|
with open(self.settings_path, 'r') as f:
|
||||||
|
self.settings.read_file(f)
|
||||||
|
|
||||||
|
def save_settings(self):
|
||||||
|
if os.path.exists(self.settings_path):
|
||||||
|
# Create a backup
|
||||||
|
copyfile(self.settings_path, self.settings_path + ".backup")
|
||||||
|
else:
|
||||||
|
# Ensure directory exists
|
||||||
|
settings_dir = os.path.dirname(self.settings_path)
|
||||||
|
os.makedirs(settings_dir, exist_ok=True)
|
||||||
|
|
||||||
|
with open(self.settings_path, 'w') as f:
|
||||||
|
self.settings.write(f)
|
||||||
|
|
||||||
|
def get_user_config(self, user: User) -> ConfigParserWithEnv:
|
||||||
|
user_settings = UserSettings.find_by_user(user)
|
||||||
|
if user_settings is not None:
|
||||||
|
user_config = ConfigParserWithEnv(defaults=self.settings, allow_no_value=True)
|
||||||
|
user_config.read_dict({
|
||||||
|
'user': user_settings.to_dict()
|
||||||
|
})
|
||||||
|
return user_config
|
||||||
|
|
||||||
|
return settings
|
||||||
|
|
||||||
|
|
||||||
|
instance: AppConfig = None
|
||||||
|
|
||||||
|
|
||||||
|
def __initialize_logger():
|
||||||
|
# Parse log level
|
||||||
|
log_level_str = instance.settings.get('global', 'LogLevel', fallback='INFO')
|
||||||
|
levels = {
|
||||||
|
'NOTSET': logging.NOTSET,
|
||||||
|
'DEBUG': logging.DEBUG,
|
||||||
|
'INFO': logging.INFO,
|
||||||
|
'WARNING': logging.WARNING,
|
||||||
|
'ERROR': logging.ERROR,
|
||||||
|
'CRITICAL': logging.CRITICAL
|
||||||
|
}
|
||||||
|
if log_level_str.upper() not in levels:
|
||||||
|
log_level_str = 'INFO'
|
||||||
|
|
||||||
|
# Init
|
||||||
|
logging.basicConfig(filename=instance.log_path, level=levels[log_level_str])
|
||||||
|
|
||||||
|
|
||||||
|
def initialize_config():
|
||||||
|
global instance
|
||||||
|
instance = AppConfig()
|
||||||
|
|
||||||
|
# Load settings
|
||||||
|
instance.load_settings()
|
||||||
|
|
||||||
|
# Initialize logger
|
||||||
|
__initialize_logger()
|
||||||
|
logging.info('Application started!')
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
from .appconfig import initialize_config
|
||||||
|
from .scheduler import initialize_scheduler
|
||||||
|
from .management import setup_synchronization_job
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
initialize_config()
|
||||||
|
initialize_scheduler()
|
||||||
|
setup_synchronization_job()
|
||||||
|
logging.info('Initialization complete.')
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
class YtManagerAppConfig(AppConfig):
|
class YtManagerAppConfig(AppConfig):
|
||||||
name = 'YtManagerApp'
|
name = 'YtManagerApp'
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from .management import SubscriptionManager
|
# There seems to be a problem related to the auto-reload functionality where ready() is called twice
|
||||||
SubscriptionManager.start_scheduler()
|
# (in different processes). This seems like a good enough workaround (other than --noreload).
|
||||||
|
if not os.getenv('RUN_MAIN', False):
|
||||||
|
from .appmain import main
|
||||||
|
main()
|
||||||
Whitespace-only changes.
@@ -0,0 +1,110 @@
|
|||||||
|
from YtManagerApp.appconfig import instance as app_config
|
||||||
|
from YtManagerApp.management.jobs.download_video import schedule_download_video
|
||||||
|
from YtManagerApp.models import Video, Subscription
|
||||||
|
from django.conf import settings
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
import mimetypes
|
||||||
|
import os
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
|
log = logging.getLogger('downloader')
|
||||||
|
|
||||||
|
|
||||||
|
def __get_subscription_config(sub: Subscription):
|
||||||
|
user_config = app_config.get_user_config(sub.user)
|
||||||
|
|
||||||
|
enabled = sub.auto_download
|
||||||
|
if enabled is None:
|
||||||
|
enabled = user_config.getboolean('user', 'AutoDownload')
|
||||||
|
|
||||||
|
global_limit = -1
|
||||||
|
if len(user_config.get('user', 'DownloadGlobalLimit')) > 0:
|
||||||
|
global_limit = user_config.getint('user', 'DownloadGlobalLimit')
|
||||||
|
|
||||||
|
limit = sub.download_limit
|
||||||
|
if limit is None:
|
||||||
|
limit = -1
|
||||||
|
if len(user_config.get('user', 'DownloadSubscriptionLimit')) > 0:
|
||||||
|
limit = user_config.getint('user', 'DownloadSubscriptionLimit')
|
||||||
|
|
||||||
|
order = sub.download_order
|
||||||
|
if order is None:
|
||||||
|
order = user_config.get('user', 'DownloadOrder')
|
||||||
|
|
||||||
|
return enabled, global_limit, limit, order
|
||||||
|
|
||||||
|
|
||||||
|
def __process_subscription(sub: Subscription):
|
||||||
|
log.info('Processing subscription %d [%s %s]', sub.id, sub.playlist_id, sub.id)
|
||||||
|
|
||||||
|
enabled, global_limit, limit, order = __get_subscription_config(sub)
|
||||||
|
log.info('Determined settings enabled=%s global_limit=%d limit=%d order="%s"', enabled, global_limit, limit, order)
|
||||||
|
|
||||||
|
if enabled:
|
||||||
|
videos_to_download = Video.objects\
|
||||||
|
.filter(subscription=sub, downloaded_path__isnull=True, watched=False)\
|
||||||
|
.order_by(order)
|
||||||
|
|
||||||
|
log.info('%d download candidates.', len(videos_to_download))
|
||||||
|
|
||||||
|
if global_limit > 0:
|
||||||
|
global_downloaded = Video.objects.filter(subscription__user=sub.user, downloaded_path__isnull=False).count()
|
||||||
|
allowed_count = max(global_limit - global_downloaded, 0)
|
||||||
|
videos_to_download = videos_to_download[0:allowed_count]
|
||||||
|
log.info('Global limit is set, can only download up to %d videos.', allowed_count)
|
||||||
|
|
||||||
|
if limit > 0:
|
||||||
|
sub_downloaded = Video.objects.filter(subscription=sub, downloaded_path__isnull=False).count()
|
||||||
|
allowed_count = max(limit - sub_downloaded, 0)
|
||||||
|
videos_to_download = videos_to_download[0:allowed_count]
|
||||||
|
log.info('Limit is set, can only download up to %d videos.', allowed_count)
|
||||||
|
|
||||||
|
# enqueue download
|
||||||
|
for video in videos_to_download:
|
||||||
|
log.info('Enqueuing video %d [%s %s] index=%d', video.id, video.video_id, video.name, video.playlist_index)
|
||||||
|
schedule_download_video(video)
|
||||||
|
|
||||||
|
log.info('Finished processing subscription %d [%s %s]', sub.id, sub.playlist_id, sub.id)
|
||||||
|
|
||||||
|
|
||||||
|
def downloader_process_all():
|
||||||
|
for subscription in Subscription.objects.all():
|
||||||
|
__process_subscription(subscription)
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_thumbnail(url, object_type, identifier, quality):
|
||||||
|
|
||||||
|
log.info('Fetching thumbnail url=%s object_type=%s identifier=%s quality=%s', url, object_type, identifier, quality)
|
||||||
|
|
||||||
|
# Make request to obtain mime type
|
||||||
|
try:
|
||||||
|
response = requests.get(url, stream=True)
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
log.error('Failed to fetch thumbnail %s. Error: %s', url, e)
|
||||||
|
return url
|
||||||
|
|
||||||
|
ext = mimetypes.guess_extension(response.headers['Content-Type'])
|
||||||
|
|
||||||
|
# Build file path
|
||||||
|
file_name = f"{identifier}-{quality}{ext}"
|
||||||
|
abs_path_dir = os.path.join(settings.MEDIA_ROOT, "thumbs", object_type)
|
||||||
|
abs_path = os.path.join(abs_path_dir, file_name)
|
||||||
|
|
||||||
|
# Store image
|
||||||
|
try:
|
||||||
|
os.makedirs(abs_path_dir, exist_ok=True)
|
||||||
|
with open(abs_path, "wb") as f:
|
||||||
|
for chunk in response.iter_content(chunk_size=1024):
|
||||||
|
if chunk:
|
||||||
|
f.write(chunk)
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
log.error('Error while downloading stream for thumbnail %s. Error: %s', url, e)
|
||||||
|
return url
|
||||||
|
except OSError as e:
|
||||||
|
log.error('Error while writing to file %s for thumbnail %s. Error: %s', abs_path, url, e)
|
||||||
|
return url
|
||||||
|
|
||||||
|
# Return
|
||||||
|
media_url = urljoin(settings.MEDIA_URL, f"thumbs/{object_type}/{file_name}")
|
||||||
|
return media_url
|
||||||
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,93 @@
|
|||||||
|
from YtManagerApp.models import Video
|
||||||
|
from YtManagerApp.scheduler import instance as scheduler
|
||||||
|
from YtManagerApp.appconfig import instance as app_config
|
||||||
|
import os
|
||||||
|
import youtube_dl
|
||||||
|
import logging
|
||||||
|
|
||||||
|
log = logging.getLogger('video_downloader')
|
||||||
|
log_youtube_dl = log.getChild('youtube_dl')
|
||||||
|
|
||||||
|
|
||||||
|
def __build_youtube_dl_params(video: Video, user_config):
|
||||||
|
# resolve path
|
||||||
|
format_dict = {
|
||||||
|
'channel': video.subscription.channel.name,
|
||||||
|
'channel_id': video.subscription.channel.channel_id,
|
||||||
|
'playlist': video.subscription.name,
|
||||||
|
'playlist_id': video.subscription.playlist_id,
|
||||||
|
'playlist_index': "{:03d}".format(1 + video.playlist_index),
|
||||||
|
'title': video.name,
|
||||||
|
'id': video.video_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
user_config.set_additional_interpolation_options(**format_dict)
|
||||||
|
|
||||||
|
download_path = user_config.get('user', 'DownloadPath')
|
||||||
|
output_pattern = user_config.get('user', 'DownloadFilePattern')
|
||||||
|
output_path = os.path.join(download_path, output_pattern)
|
||||||
|
output_path = os.path.normpath(output_path)
|
||||||
|
|
||||||
|
youtube_dl_params = {
|
||||||
|
'logger': log_youtube_dl,
|
||||||
|
'format': user_config.get('user', 'DownloadFormat'),
|
||||||
|
'outtmpl': output_path,
|
||||||
|
'writethumbnail': True,
|
||||||
|
'writedescription': True,
|
||||||
|
'writesubtitles': user_config.getboolean('user', 'DownloadSubtitles'),
|
||||||
|
'writeautomaticsub': user_config.getboolean('user', 'DownloadAutogeneratedSubtitles'),
|
||||||
|
'allsubtitles': user_config.getboolean('user', 'DownloadSubtitlesAll'),
|
||||||
|
'postprocessors': [
|
||||||
|
{
|
||||||
|
'key': 'FFmpegMetadataPP'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
sub_langs = user_config.get('user', 'DownloadSubtitlesLangs').split(',')
|
||||||
|
sub_langs = [i.strip() for i in sub_langs]
|
||||||
|
if len(sub_langs) > 0:
|
||||||
|
youtube_dl_params['subtitleslangs'] = sub_langs
|
||||||
|
|
||||||
|
sub_format = user_config.get('user', 'DownloadSubtitlesFormat')
|
||||||
|
if len(sub_format) > 0:
|
||||||
|
youtube_dl_params['subtitlesformat'] = sub_format
|
||||||
|
|
||||||
|
return youtube_dl_params, output_path
|
||||||
|
|
||||||
|
|
||||||
|
def download_video(video: Video, attempt: int = 1):
|
||||||
|
|
||||||
|
log.info('Downloading video %d [%s %s]', video.id, video.video_id, video.name)
|
||||||
|
|
||||||
|
user_config = app_config.get_user_config(video.subscription.user)
|
||||||
|
max_attempts = user_config.getint('user', 'DownloadMaxAttempts', fallback=3)
|
||||||
|
|
||||||
|
youtube_dl_params, output_path = __build_youtube_dl_params(video, user_config)
|
||||||
|
with youtube_dl.YoutubeDL(youtube_dl_params) as yt:
|
||||||
|
ret = yt.download(["https://www.youtube.com/watch?v=" + video.video_id])
|
||||||
|
|
||||||
|
log.info('Download finished with code %d', ret)
|
||||||
|
|
||||||
|
if ret == 0:
|
||||||
|
video.downloaded_path = output_path
|
||||||
|
video.save()
|
||||||
|
log.error('Video %d [%s %s] downloaded successfully!', video.id, video.video_id, video.name)
|
||||||
|
|
||||||
|
elif attempt <= max_attempts:
|
||||||
|
log.warning('Re-enqueueing video (attempt %d/%d)', attempt, max_attempts)
|
||||||
|
scheduler.add_job(download_video, args=[video, attempt + 1])
|
||||||
|
|
||||||
|
else:
|
||||||
|
log.error('Multiple attempts to download video %d [%s %s] failed!', video.id, video.video_id, video.name)
|
||||||
|
video.downloaded_path = ''
|
||||||
|
video.save()
|
||||||
|
|
||||||
|
|
||||||
|
def schedule_download_video(video: Video):
|
||||||
|
"""
|
||||||
|
Schedules a download video job to run immediately.
|
||||||
|
:param video:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
scheduler.add_job(download_video, args=[video, 1])
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
from YtManagerApp.models import *
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
def synchronize():
|
||||||
|
logger = logging.getLogger('sync')
|
||||||
|
|
||||||
|
logger.info("Running scheduled synchronization... ")
|
||||||
|
|
||||||
|
# Sync subscribed playlists/channels
|
||||||
|
yt_api = YoutubeAPI.build_public()
|
||||||
|
for subscription in Subscription.objects.all():
|
||||||
|
SubscriptionManager.__synchronize(subscription, yt_api)
|
||||||
|
|
||||||
|
# Fetch thumbnails
|
||||||
|
logger.info("Fetching channel thumbnails... ")
|
||||||
|
for ch in Channel.objects.filter(icon_default__istartswith='http'):
|
||||||
|
ch.icon_default = SubscriptionManager.__fetch_thumbnail(ch.icon_default, 'channel', ch.channel_id, 'default')
|
||||||
|
ch.save()
|
||||||
|
|
||||||
|
for ch in Channel.objects.filter(icon_best__istartswith='http'):
|
||||||
|
ch.icon_best = SubscriptionManager.__fetch_thumbnail(ch.icon_best, 'channel', ch.channel_id, 'best')
|
||||||
|
ch.save()
|
||||||
|
|
||||||
|
logger.info("Fetching subscription thumbnails... ")
|
||||||
|
for sub in Subscription.objects.filter(icon_default__istartswith='http'):
|
||||||
|
sub.icon_default = SubscriptionManager.__fetch_thumbnail(sub.icon_default, 'sub', sub.playlist_id, 'default')
|
||||||
|
sub.save()
|
||||||
|
|
||||||
|
for sub in Subscription.objects.filter(icon_best__istartswith='http'):
|
||||||
|
sub.icon_best = SubscriptionManager.__fetch_thumbnail(sub.icon_best, 'sub', sub.playlist_id, 'best')
|
||||||
|
sub.save()
|
||||||
|
|
||||||
|
logger.info("Fetching video thumbnails... ")
|
||||||
|
for vid in Video.objects.filter(icon_default__istartswith='http'):
|
||||||
|
vid.icon_default = SubscriptionManager.__fetch_thumbnail(vid.icon_default, 'video', vid.video_id, 'default')
|
||||||
|
vid.save()
|
||||||
|
|
||||||
|
for vid in Video.objects.filter(icon_best__istartswith='http'):
|
||||||
|
vid.icon_best = SubscriptionManager.__fetch_thumbnail(vid.icon_best, 'video', vid.video_id, 'best')
|
||||||
|
vid.save()
|
||||||
|
|
||||||
|
print("Downloading videos...")
|
||||||
|
Downloader.download_all()
|
||||||
|
|
||||||
|
print("Synchronization finished.")
|
||||||
@@ -1,7 +1,18 @@
|
|||||||
from .models import SubscriptionFolder, Subscription, Video, Channel
|
from YtManagerApp.models import SubscriptionFolder, Subscription, Video, Channel
|
||||||
from .youtube import YoutubeAPI, YoutubeChannelInfo, YoutubePlaylistItem
|
from YtManagerApp.utils.youtube import YoutubeAPI, YoutubeChannelInfo, YoutubePlaylistItem
|
||||||
|
from django.conf import settings
|
||||||
from apscheduler.schedulers.background import BackgroundScheduler
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
import os
|
import os
|
||||||
|
import os.path
|
||||||
|
import requests
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
import mimetypes
|
||||||
|
import youtube_dl
|
||||||
|
|
||||||
|
from YtManagerApp.scheduler import instance as scheduler
|
||||||
|
from YtManagerApp.appconfig import instance as app_config
|
||||||
|
from apscheduler.triggers.cron import CronTrigger
|
||||||
|
|
||||||
|
|
||||||
class FolderManager(object):
|
class FolderManager(object):
|
||||||
|
|
||||||
@@ -45,6 +56,18 @@ class FolderManager(object):
|
|||||||
folder = SubscriptionFolder.objects.get(id=fid)
|
folder = SubscriptionFolder.objects.get(id=fid)
|
||||||
folder.delete()
|
folder.delete()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def list_videos(fid: int):
|
||||||
|
folder = SubscriptionFolder.objects.get(id=fid)
|
||||||
|
folder_list = []
|
||||||
|
queue = [folder]
|
||||||
|
while len(queue) > 0:
|
||||||
|
folder = queue.pop()
|
||||||
|
folder_list.append(folder)
|
||||||
|
queue.extend(SubscriptionFolder.objects.filter(parent=folder))
|
||||||
|
|
||||||
|
return Video.objects.filter(subscription__parent_folder__in=folder_list).order_by('-publish_date')
|
||||||
|
|
||||||
|
|
||||||
class SubscriptionManager(object):
|
class SubscriptionManager(object):
|
||||||
__scheduler = BackgroundScheduler()
|
__scheduler = BackgroundScheduler()
|
||||||
@@ -53,7 +76,6 @@ class SubscriptionManager(object):
|
|||||||
def create_or_edit(sid, url, name, parent_id):
|
def create_or_edit(sid, url, name, parent_id):
|
||||||
# Create or edit
|
# Create or edit
|
||||||
if sid == '#':
|
if sid == '#':
|
||||||
sub = Subscription()
|
|
||||||
SubscriptionManager.create(url, parent_id, YoutubeAPI.build_public())
|
SubscriptionManager.create(url, parent_id, YoutubeAPI.build_public())
|
||||||
else:
|
else:
|
||||||
sub = Subscription.objects.get(id=int(sid))
|
sub = Subscription.objects.get(id=int(sid))
|
||||||
@@ -100,6 +122,11 @@ class SubscriptionManager(object):
|
|||||||
|
|
||||||
sub.save()
|
sub.save()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def list_videos(fid: int):
|
||||||
|
sub = Subscription.objects.get(id=fid)
|
||||||
|
return Video.objects.filter(subscription=sub).order_by('playlist_index')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __get_or_create_channel(url_type, url_id, yt_api: YoutubeAPI):
|
def __get_or_create_channel(url_type, url_id, yt_api: YoutubeAPI):
|
||||||
|
|
||||||
@@ -173,12 +200,79 @@ class SubscriptionManager(object):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def __synchronize_all():
|
def __synchronize_all():
|
||||||
print("Running scheduled synchronization... ")
|
print("Running scheduled synchronization... ")
|
||||||
|
|
||||||
|
# Sync subscribed playlists/channels
|
||||||
yt_api = YoutubeAPI.build_public()
|
yt_api = YoutubeAPI.build_public()
|
||||||
for subscription in Subscription.objects.all():
|
for subscription in Subscription.objects.all():
|
||||||
SubscriptionManager.__synchronize(subscription, yt_api)
|
SubscriptionManager.__synchronize(subscription, yt_api)
|
||||||
|
|
||||||
|
# Fetch thumbnails
|
||||||
|
print("Fetching channel thumbnails... ")
|
||||||
|
for ch in Channel.objects.filter(icon_default__istartswith='http'):
|
||||||
|
ch.icon_default = SubscriptionManager.__fetch_thumbnail(ch.icon_default, 'channel', ch.channel_id, 'default')
|
||||||
|
ch.save()
|
||||||
|
|
||||||
|
for ch in Channel.objects.filter(icon_best__istartswith='http'):
|
||||||
|
ch.icon_best = SubscriptionManager.__fetch_thumbnail(ch.icon_best, 'channel', ch.channel_id, 'best')
|
||||||
|
ch.save()
|
||||||
|
|
||||||
|
print("Fetching subscription thumbnails... ")
|
||||||
|
for sub in Subscription.objects.filter(icon_default__istartswith='http'):
|
||||||
|
sub.icon_default = SubscriptionManager.__fetch_thumbnail(sub.icon_default, 'sub', sub.playlist_id, 'default')
|
||||||
|
sub.save()
|
||||||
|
|
||||||
|
for sub in Subscription.objects.filter(icon_best__istartswith='http'):
|
||||||
|
sub.icon_best = SubscriptionManager.__fetch_thumbnail(sub.icon_best, 'sub', sub.playlist_id, 'best')
|
||||||
|
sub.save()
|
||||||
|
|
||||||
|
print("Fetching video thumbnails... ")
|
||||||
|
for vid in Video.objects.filter(icon_default__istartswith='http'):
|
||||||
|
vid.icon_default = SubscriptionManager.__fetch_thumbnail(vid.icon_default, 'video', vid.video_id, 'default')
|
||||||
|
vid.save()
|
||||||
|
|
||||||
|
for vid in Video.objects.filter(icon_best__istartswith='http'):
|
||||||
|
vid.icon_best = SubscriptionManager.__fetch_thumbnail(vid.icon_best, 'video', vid.video_id, 'best')
|
||||||
|
vid.save()
|
||||||
|
|
||||||
|
print("Downloading videos...")
|
||||||
|
Downloader.download_all()
|
||||||
|
|
||||||
|
print("Synchronization finished.")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __fetch_thumbnail(url, object_type, identifier, quality):
|
||||||
|
|
||||||
|
# Make request to obtain mime type
|
||||||
|
response = requests.get(url, stream=True)
|
||||||
|
ext = mimetypes.guess_extension(response.headers['Content-Type'])
|
||||||
|
|
||||||
|
# Build file path
|
||||||
|
file_name = f"{identifier}-{quality}{ext}"
|
||||||
|
abs_path_dir = os.path.join(settings.MEDIA_ROOT, "thumbs", object_type)
|
||||||
|
abs_path = os.path.join(abs_path_dir, file_name)
|
||||||
|
|
||||||
|
# Store image
|
||||||
|
os.makedirs(abs_path_dir, exist_ok=True)
|
||||||
|
with open(abs_path, "wb") as f:
|
||||||
|
for chunk in response.iter_content(chunk_size=1024):
|
||||||
|
if chunk:
|
||||||
|
f.write(chunk)
|
||||||
|
|
||||||
|
# Return
|
||||||
|
media_url = urljoin(settings.MEDIA_URL, f"thumbs/{object_type}/{file_name}")
|
||||||
|
return media_url
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def start_scheduler():
|
def start_scheduler():
|
||||||
SubscriptionManager.__scheduler.add_job(SubscriptionManager.__synchronize_all, 'cron',
|
SubscriptionManager.__scheduler.add_job(SubscriptionManager.__synchronize_all, 'cron',
|
||||||
hour='*', minute=44, max_instances=1)
|
hour='*', minute=38, max_instances=1)
|
||||||
SubscriptionManager.__scheduler.start()
|
SubscriptionManager.__scheduler.start()
|
||||||
|
|
||||||
|
|
||||||
|
def setup_synchronization_job():
|
||||||
|
trigger = CronTrigger.from_crontab(app_config.get('global', 'SynchronizationSchedule'))
|
||||||
|
scheduler.add_job(synchronize_all, trigger, max_instances=1)
|
||||||
|
|
||||||
|
|
||||||
|
def synchronize_all():
|
||||||
|
pass
|
||||||
Whitespace-only changes.
@@ -0,0 +1,33 @@
|
|||||||
|
# Generated by Django 2.1.2 on 2018-10-08 23:09
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('YtManagerApp', '0006_auto_20181008_0037'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='subscription',
|
||||||
|
name='downloader_enabled',
|
||||||
|
field=models.BooleanField(null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='subscription',
|
||||||
|
name='downloader_limit',
|
||||||
|
field=models.IntegerField(null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='subscription',
|
||||||
|
name='downloader_order',
|
||||||
|
field=models.TextField(null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='subscription',
|
||||||
|
name='manager_delete_after_watched',
|
||||||
|
field=models.BooleanField(null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,4 +1,67 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
|
||||||
|
class UserSettings(models.Model):
|
||||||
|
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||||
|
mark_deleted_as_watched = models.BooleanField(null=True)
|
||||||
|
delete_watched = models.BooleanField(null=True)
|
||||||
|
auto_download = models.BooleanField(null=True)
|
||||||
|
download_global_limit = models.IntegerField(null=True)
|
||||||
|
download_subscription_limit = models.IntegerField(null=True)
|
||||||
|
download_order = models.TextField(null=True)
|
||||||
|
download_path = models.TextField(null=True)
|
||||||
|
download_file_pattern = models.TextField(null=True)
|
||||||
|
download_format = models.TextField(null=True)
|
||||||
|
download_subtitles = models.BooleanField(null=True)
|
||||||
|
download_autogenerated_subtitles = models.BooleanField(null=True)
|
||||||
|
download_subtitles_all = models.BooleanField(null=True)
|
||||||
|
download_subtitles_langs = models.TextField(null=True)
|
||||||
|
download_subtitles_format = models.TextField(null=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def find_by_user(user: User):
|
||||||
|
result = UserSettings.objects.filter(user=user)
|
||||||
|
if len(result) > 0:
|
||||||
|
return result.first()
|
||||||
|
return None
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.user)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
ret = {}
|
||||||
|
|
||||||
|
if self.mark_deleted_as_watched is not None:
|
||||||
|
ret['MarkDeletedAsWatched'] = self.mark_deleted_as_watched
|
||||||
|
if self.delete_watched is not None:
|
||||||
|
ret['DeleteWatched'] = self.delete_watched
|
||||||
|
if self.auto_download is not None:
|
||||||
|
ret['AutoDownload'] = self.auto_download
|
||||||
|
if self.download_global_limit is not None:
|
||||||
|
ret['DownloadGlobalLimit'] = self.download_global_limit
|
||||||
|
if self.download_subscription_limit is not None:
|
||||||
|
ret['DownloadSubscriptionLimit'] = self.download_subscription_limit
|
||||||
|
if self.download_order is not None:
|
||||||
|
ret['DownloadOrder'] = self.download_order
|
||||||
|
if self.download_path is not None:
|
||||||
|
ret['DownloadPath'] = self.download_path
|
||||||
|
if self.download_file_pattern is not None:
|
||||||
|
ret['DownloadFilePattern'] = self.download_file_pattern
|
||||||
|
if self.download_format is not None:
|
||||||
|
ret['DownloadFormat'] = self.download_format
|
||||||
|
if self.download_subtitles is not None:
|
||||||
|
ret['DownloadSubtitles'] = self.download_subtitles
|
||||||
|
if self.download_autogenerated_subtitles is not None:
|
||||||
|
ret['DownloadAutogeneratedSubtitles'] = self.download_autogenerated_subtitles
|
||||||
|
if self.download_subtitles_all is not None:
|
||||||
|
ret['DownloadSubtitlesAll'] = self.download_subtitles_all
|
||||||
|
if self.download_subtitles_langs is not None:
|
||||||
|
ret['DownloadSubtitlesLangs'] = self.download_subtitles_langs
|
||||||
|
if self.download_subtitles_format is not None:
|
||||||
|
ret['DownloadSubtitlesFormat'] = self.download_subtitles_format
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
class SubscriptionFolder(models.Model):
|
class SubscriptionFolder(models.Model):
|
||||||
@@ -52,6 +115,13 @@ class Subscription(models.Model):
|
|||||||
channel = models.ForeignKey(Channel, on_delete=models.CASCADE)
|
channel = models.ForeignKey(Channel, on_delete=models.CASCADE)
|
||||||
icon_default = models.TextField()
|
icon_default = models.TextField()
|
||||||
icon_best = models.TextField()
|
icon_best = models.TextField()
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
# overrides
|
||||||
|
auto_download = models.BooleanField(null=True)
|
||||||
|
download_limit = models.IntegerField(null=True)
|
||||||
|
download_order = models.TextField(null=True)
|
||||||
|
manager_delete_after_watched = models.BooleanField(null=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import logging
|
||||||
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
|
|
||||||
|
from .appconfig import instance as app_config
|
||||||
|
|
||||||
|
instance: BackgroundScheduler = None
|
||||||
|
|
||||||
|
|
||||||
|
def initialize_scheduler():
|
||||||
|
global instance
|
||||||
|
logger = logging.getLogger('scheduler')
|
||||||
|
executors = {
|
||||||
|
'default': {
|
||||||
|
'type': 'threadpool',
|
||||||
|
'max_workers': int(app_config.get('global', 'SchedulerConcurrency'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
instance = BackgroundScheduler(logger=logger, executors=executors)
|
||||||
|
instance.start()
|
||||||
@@ -27,5 +27,28 @@
|
|||||||
transform: rotate(0deg); }
|
transform: rotate(0deg); }
|
||||||
100% {
|
100% {
|
||||||
transform: rotate(360deg); } }
|
transform: rotate(360deg); } }
|
||||||
|
.video-gallery .card-wrapper {
|
||||||
|
padding: 0.4rem;
|
||||||
|
margin-bottom: .5rem; }
|
||||||
|
.video-gallery .card .card-body {
|
||||||
|
padding: .75rem; }
|
||||||
|
.video-gallery .card .card-text {
|
||||||
|
font-size: 10pt;
|
||||||
|
margin-bottom: .5rem; }
|
||||||
|
.video-gallery .card .card-title {
|
||||||
|
font-size: 11pt;
|
||||||
|
margin-bottom: .5rem; }
|
||||||
|
.video-gallery .card .card-title .badge {
|
||||||
|
font-size: 8pt; }
|
||||||
|
.video-gallery .card .card-footer {
|
||||||
|
padding: .5rem .75rem; }
|
||||||
|
.video-gallery .card .card-more {
|
||||||
|
margin-right: -0.25rem; }
|
||||||
|
.video-gallery .card .card-more:hover {
|
||||||
|
text-decoration: none; }
|
||||||
|
.video-gallery .video-icon-yes {
|
||||||
|
color: #6c757d; }
|
||||||
|
.video-gallery .video-icon-no {
|
||||||
|
color: #cccccc; }
|
||||||
|
|
||||||
/*# sourceMappingURL=style.css.map */
|
/*# sourceMappingURL=style.css.map */
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"mappings": "AAEA,gCAAgC;AAChC,wBAAyB;EACrB,OAAO,EAAE,OAAO;;AAGpB,wBAAyB;EACrB,OAAO,EAAE,OAAO;;AAGpB,uBAAuB;AACvB,kBAAmB;EACf,OAAO,EAAE,YAAY;EACrB,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;;AAGhB,wBAAyB;EACrB,OAAO,EAAE,GAAG;EACZ,OAAO,EAAE,KAAK;EACd,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;EACZ,MAAM,EAAE,GAAG;EACX,aAAa,EAAE,GAAG;EAClB,MAAM,EAAE,iBAAuB;EAC/B,YAAY,EAAE,uCAAmD;EACjE,SAAS,EAAE,sCAAsC;;AAGrD,4BAOC;EANG,EAAG;IACC,SAAS,EAAE,YAAY;EAE3B,IAAK;IACD,SAAS,EAAE,cAAc",
|
"mappings": "AAEA,gCAAgC;AAChC,wBAAyB;EACrB,OAAO,EAAE,OAAO;;AAGpB,wBAAyB;EACrB,OAAO,EAAE,OAAO;;AAGpB,uBAAuB;AACvB,kBAAmB;EACf,OAAO,EAAE,YAAY;EACrB,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;;AAGhB,wBAAyB;EACrB,OAAO,EAAE,GAAG;EACZ,OAAO,EAAE,KAAK;EACd,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;EACZ,MAAM,EAAE,GAAG;EACX,aAAa,EAAE,GAAG;EAClB,MAAM,EAAE,iBAAuB;EAC/B,YAAY,EAAE,uCAAmD;EACjE,SAAS,EAAE,sCAAsC;;AAGrD,4BAOC;EANG,EAAG;IACC,SAAS,EAAE,YAAY;EAE3B,IAAK;IACD,SAAS,EAAE,cAAc;AAK7B,4BAAc;EACV,OAAO,EAAE,MAAM;EACf,aAAa,EAAE,KAAK;AAGpB,+BAAW;EACP,OAAO,EAAE,MAAM;AAEnB,+BAAW;EACP,SAAS,EAAE,IAAI;EACf,aAAa,EAAE,KAAK;AAExB,gCAAY;EACR,SAAS,EAAE,IAAI;EACf,aAAa,EAAE,KAAK;EAEpB,uCAAO;IACH,SAAS,EAAE,GAAG;AAGtB,iCAAa;EACT,OAAO,EAAE,YAAY;AAGzB,+BAAW;EACP,YAAY,EAAE,QAAQ;EACtB,qCAAQ;IACJ,eAAe,EAAE,IAAI;AAKjC,8BAAgB;EACZ,KAAK,EAAE,OAAO;AAElB,6BAAe;EACX,KAAK,EAAE,OAAO",
|
||||||
"sources": ["style.scss"],
|
"sources": ["style.scss"],
|
||||||
"names": [],
|
"names": [],
|
||||||
"file": "style.css"
|
"file": "style.css"
|
||||||
|
|||||||
@@ -35,4 +35,46 @@ $accent-color: #007bff;
|
|||||||
100% {
|
100% {
|
||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-gallery {
|
||||||
|
.card-wrapper {
|
||||||
|
padding: 0.4rem;
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
.card-body {
|
||||||
|
padding: .75rem;
|
||||||
|
}
|
||||||
|
.card-text {
|
||||||
|
font-size: 10pt;
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
}
|
||||||
|
.card-title {
|
||||||
|
font-size: 11pt;
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
font-size: 8pt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.card-footer {
|
||||||
|
padding: .5rem .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-more {
|
||||||
|
margin-right: -0.25rem;
|
||||||
|
&:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-icon-yes {
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
.video-icon-no {
|
||||||
|
color: #cccccc;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
Copyright (c) 2014, Stephen Hutchings (http://www.s-ings.com/).
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
|
||||||
|
This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
SIL OPEN FONT LICENSE
|
||||||
|
|
||||||
|
Version 1.1 - 26 February 2007
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting — in part or in whole — any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
@@ -0,0 +1,406 @@
|
|||||||
|
<!DOCTYPE html><html><head><!--[if lt IE 9]><script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script><![endif]--><meta charset="UTF-8"><style type="text/css">body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 18px;
|
||||||
|
color: #333;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
.page-header {
|
||||||
|
width: 100%;
|
||||||
|
position: fixed;
|
||||||
|
background: #fff;
|
||||||
|
z-index: 10;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.page-header small {
|
||||||
|
font-size: 0.6em;
|
||||||
|
color: #666;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
.page-header .container {
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
.container,
|
||||||
|
#preview {
|
||||||
|
width: 940px;
|
||||||
|
margin: auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
#icons {
|
||||||
|
margin-top: 112px;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
#icons .icon {
|
||||||
|
float: left;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
</style><style type="text/css">
|
||||||
|
@charset 'UTF-8';
|
||||||
|
/* @FONT-FACE loads font into browser */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'typicons';
|
||||||
|
src: url('typicons.eot');
|
||||||
|
src: url('typicons.eot?#iefix') format('embedded-opentype'),
|
||||||
|
url('typicons.woff') format('woff'),
|
||||||
|
url('typicons.ttf') format('truetype'),
|
||||||
|
url('typicons.svg#typicons') format('svg');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
/* :before psuedo-selector inserts and styles icon */
|
||||||
|
.typcn:before {
|
||||||
|
font-family: 'typicons';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: normal;
|
||||||
|
speak: none;
|
||||||
|
display: inline-block;
|
||||||
|
text-decoration: inherit;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
font-size: 1em;
|
||||||
|
text-align: center;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
font-smoothing: antialiased;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style type="text/css">
|
||||||
|
/* Code for individual icons */
|
||||||
|
.typcn-adjust-brightness:before { content: '\e000'; } /* '' */
|
||||||
|
.typcn-adjust-contrast:before { content: '\e001'; } /* '' */
|
||||||
|
.typcn-anchor-outline:before { content: '\e002'; } /* '' */
|
||||||
|
.typcn-anchor:before { content: '\e003'; } /* '' */
|
||||||
|
.typcn-archive:before { content: '\e004'; } /* '' */
|
||||||
|
.typcn-arrow-back-outline:before { content: '\e005'; } /* '' */
|
||||||
|
.typcn-arrow-back:before { content: '\e006'; } /* '' */
|
||||||
|
.typcn-arrow-down-outline:before { content: '\e007'; } /* '' */
|
||||||
|
.typcn-arrow-down-thick:before { content: '\e008'; } /* '' */
|
||||||
|
.typcn-arrow-down:before { content: '\e009'; } /* '' */
|
||||||
|
.typcn-arrow-forward-outline:before { content: '\e00a'; } /* '' */
|
||||||
|
.typcn-arrow-forward:before { content: '\e00b'; } /* '' */
|
||||||
|
.typcn-arrow-left-outline:before { content: '\e00c'; } /* '' */
|
||||||
|
.typcn-arrow-left-thick:before { content: '\e00d'; } /* '' */
|
||||||
|
.typcn-arrow-left:before { content: '\e00e'; } /* '' */
|
||||||
|
.typcn-arrow-loop-outline:before { content: '\e00f'; } /* '' */
|
||||||
|
.typcn-arrow-loop:before { content: '\e010'; } /* '' */
|
||||||
|
.typcn-arrow-maximise-outline:before { content: '\e011'; } /* '' */
|
||||||
|
.typcn-arrow-maximise:before { content: '\e012'; } /* '' */
|
||||||
|
.typcn-arrow-minimise-outline:before { content: '\e013'; } /* '' */
|
||||||
|
.typcn-arrow-minimise:before { content: '\e014'; } /* '' */
|
||||||
|
.typcn-arrow-move-outline:before { content: '\e015'; } /* '' */
|
||||||
|
.typcn-arrow-move:before { content: '\e016'; } /* '' */
|
||||||
|
.typcn-arrow-repeat-outline:before { content: '\e017'; } /* '' */
|
||||||
|
.typcn-arrow-repeat:before { content: '\e018'; } /* '' */
|
||||||
|
.typcn-arrow-right-outline:before { content: '\e019'; } /* '' */
|
||||||
|
.typcn-arrow-right-thick:before { content: '\e01a'; } /* '' */
|
||||||
|
.typcn-arrow-right:before { content: '\e01b'; } /* '' */
|
||||||
|
.typcn-arrow-shuffle:before { content: '\e01c'; } /* '' */
|
||||||
|
.typcn-arrow-sorted-down:before { content: '\e01d'; } /* '' */
|
||||||
|
.typcn-arrow-sorted-up:before { content: '\e01e'; } /* '' */
|
||||||
|
.typcn-arrow-sync-outline:before { content: '\e01f'; } /* '' */
|
||||||
|
.typcn-arrow-sync:before { content: '\e020'; } /* '' */
|
||||||
|
.typcn-arrow-unsorted:before { content: '\e021'; } /* '' */
|
||||||
|
.typcn-arrow-up-outline:before { content: '\e022'; } /* '' */
|
||||||
|
.typcn-arrow-up-thick:before { content: '\e023'; } /* '' */
|
||||||
|
.typcn-arrow-up:before { content: '\e024'; } /* '' */
|
||||||
|
.typcn-at:before { content: '\e025'; } /* '' */
|
||||||
|
.typcn-attachment-outline:before { content: '\e026'; } /* '' */
|
||||||
|
.typcn-attachment:before { content: '\e027'; } /* '' */
|
||||||
|
.typcn-backspace-outline:before { content: '\e028'; } /* '' */
|
||||||
|
.typcn-backspace:before { content: '\e029'; } /* '' */
|
||||||
|
.typcn-battery-charge:before { content: '\e02a'; } /* '' */
|
||||||
|
.typcn-battery-full:before { content: '\e02b'; } /* '' */
|
||||||
|
.typcn-battery-high:before { content: '\e02c'; } /* '' */
|
||||||
|
.typcn-battery-low:before { content: '\e02d'; } /* '' */
|
||||||
|
.typcn-battery-mid:before { content: '\e02e'; } /* '' */
|
||||||
|
.typcn-beaker:before { content: '\e02f'; } /* '' */
|
||||||
|
.typcn-beer:before { content: '\e030'; } /* '' */
|
||||||
|
.typcn-bell:before { content: '\e031'; } /* '' */
|
||||||
|
.typcn-book:before { content: '\e032'; } /* '' */
|
||||||
|
.typcn-bookmark:before { content: '\e033'; } /* '' */
|
||||||
|
.typcn-briefcase:before { content: '\e034'; } /* '' */
|
||||||
|
.typcn-brush:before { content: '\e035'; } /* '' */
|
||||||
|
.typcn-business-card:before { content: '\e036'; } /* '' */
|
||||||
|
.typcn-calculator:before { content: '\e037'; } /* '' */
|
||||||
|
.typcn-calendar-outline:before { content: '\e038'; } /* '' */
|
||||||
|
.typcn-calendar:before { content: '\e039'; } /* '' */
|
||||||
|
.typcn-camera-outline:before { content: '\e03a'; } /* '' */
|
||||||
|
.typcn-camera:before { content: '\e03b'; } /* '' */
|
||||||
|
.typcn-cancel-outline:before { content: '\e03c'; } /* '' */
|
||||||
|
.typcn-cancel:before { content: '\e03d'; } /* '' */
|
||||||
|
.typcn-chart-area-outline:before { content: '\e03e'; } /* '' */
|
||||||
|
.typcn-chart-area:before { content: '\e03f'; } /* '' */
|
||||||
|
.typcn-chart-bar-outline:before { content: '\e040'; } /* '' */
|
||||||
|
.typcn-chart-bar:before { content: '\e041'; } /* '' */
|
||||||
|
.typcn-chart-line-outline:before { content: '\e042'; } /* '' */
|
||||||
|
.typcn-chart-line:before { content: '\e043'; } /* '' */
|
||||||
|
.typcn-chart-pie-outline:before { content: '\e044'; } /* '' */
|
||||||
|
.typcn-chart-pie:before { content: '\e045'; } /* '' */
|
||||||
|
.typcn-chevron-left-outline:before { content: '\e046'; } /* '' */
|
||||||
|
.typcn-chevron-left:before { content: '\e047'; } /* '' */
|
||||||
|
.typcn-chevron-right-outline:before { content: '\e048'; } /* '' */
|
||||||
|
.typcn-chevron-right:before { content: '\e049'; } /* '' */
|
||||||
|
.typcn-clipboard:before { content: '\e04a'; } /* '' */
|
||||||
|
.typcn-cloud-storage:before { content: '\e04b'; } /* '' */
|
||||||
|
.typcn-cloud-storage-outline:before { content: '\e054'; } /* '' */
|
||||||
|
.typcn-code-outline:before { content: '\e04c'; } /* '' */
|
||||||
|
.typcn-code:before { content: '\e04d'; } /* '' */
|
||||||
|
.typcn-coffee:before { content: '\e04e'; } /* '' */
|
||||||
|
.typcn-cog-outline:before { content: '\e04f'; } /* '' */
|
||||||
|
.typcn-cog:before { content: '\e050'; } /* '' */
|
||||||
|
.typcn-compass:before { content: '\e051'; } /* '' */
|
||||||
|
.typcn-contacts:before { content: '\e052'; } /* '' */
|
||||||
|
.typcn-credit-card:before { content: '\e053'; } /* '' */
|
||||||
|
.typcn-css3:before { content: '\e055'; } /* '' */
|
||||||
|
.typcn-database:before { content: '\e056'; } /* '' */
|
||||||
|
.typcn-delete-outline:before { content: '\e057'; } /* '' */
|
||||||
|
.typcn-delete:before { content: '\e058'; } /* '' */
|
||||||
|
.typcn-device-desktop:before { content: '\e059'; } /* '' */
|
||||||
|
.typcn-device-laptop:before { content: '\e05a'; } /* '' */
|
||||||
|
.typcn-device-phone:before { content: '\e05b'; } /* '' */
|
||||||
|
.typcn-device-tablet:before { content: '\e05c'; } /* '' */
|
||||||
|
.typcn-directions:before { content: '\e05d'; } /* '' */
|
||||||
|
.typcn-divide-outline:before { content: '\e05e'; } /* '' */
|
||||||
|
.typcn-divide:before { content: '\e05f'; } /* '' */
|
||||||
|
.typcn-document-add:before { content: '\e060'; } /* '' */
|
||||||
|
.typcn-document-delete:before { content: '\e061'; } /* '' */
|
||||||
|
.typcn-document-text:before { content: '\e062'; } /* '' */
|
||||||
|
.typcn-document:before { content: '\e063'; } /* '' */
|
||||||
|
.typcn-download-outline:before { content: '\e064'; } /* '' */
|
||||||
|
.typcn-download:before { content: '\e065'; } /* '' */
|
||||||
|
.typcn-dropbox:before { content: '\e066'; } /* '' */
|
||||||
|
.typcn-edit:before { content: '\e067'; } /* '' */
|
||||||
|
.typcn-eject-outline:before { content: '\e068'; } /* '' */
|
||||||
|
.typcn-eject:before { content: '\e069'; } /* '' */
|
||||||
|
.typcn-equals-outline:before { content: '\e06a'; } /* '' */
|
||||||
|
.typcn-equals:before { content: '\e06b'; } /* '' */
|
||||||
|
.typcn-export-outline:before { content: '\e06c'; } /* '' */
|
||||||
|
.typcn-export:before { content: '\e06d'; } /* '' */
|
||||||
|
.typcn-eye-outline:before { content: '\e06e'; } /* '' */
|
||||||
|
.typcn-eye:before { content: '\e06f'; } /* '' */
|
||||||
|
.typcn-feather:before { content: '\e070'; } /* '' */
|
||||||
|
.typcn-film:before { content: '\e071'; } /* '' */
|
||||||
|
.typcn-filter:before { content: '\e072'; } /* '' */
|
||||||
|
.typcn-flag-outline:before { content: '\e073'; } /* '' */
|
||||||
|
.typcn-flag:before { content: '\e074'; } /* '' */
|
||||||
|
.typcn-flash-outline:before { content: '\e075'; } /* '' */
|
||||||
|
.typcn-flash:before { content: '\e076'; } /* '' */
|
||||||
|
.typcn-flow-children:before { content: '\e077'; } /* '' */
|
||||||
|
.typcn-flow-merge:before { content: '\e078'; } /* '' */
|
||||||
|
.typcn-flow-parallel:before { content: '\e079'; } /* '' */
|
||||||
|
.typcn-flow-switch:before { content: '\e07a'; } /* '' */
|
||||||
|
.typcn-folder-add:before { content: '\e07b'; } /* '' */
|
||||||
|
.typcn-folder-delete:before { content: '\e07c'; } /* '' */
|
||||||
|
.typcn-folder-open:before { content: '\e07d'; } /* '' */
|
||||||
|
.typcn-folder:before { content: '\e07e'; } /* '' */
|
||||||
|
.typcn-gift:before { content: '\e07f'; } /* '' */
|
||||||
|
.typcn-globe-outline:before { content: '\e080'; } /* '' */
|
||||||
|
.typcn-globe:before { content: '\e081'; } /* '' */
|
||||||
|
.typcn-group-outline:before { content: '\e082'; } /* '' */
|
||||||
|
.typcn-group:before { content: '\e083'; } /* '' */
|
||||||
|
.typcn-headphones:before { content: '\e084'; } /* '' */
|
||||||
|
.typcn-heart-full-outline:before { content: '\e085'; } /* '' */
|
||||||
|
.typcn-heart-half-outline:before { content: '\e086'; } /* '' */
|
||||||
|
.typcn-heart-outline:before { content: '\e087'; } /* '' */
|
||||||
|
.typcn-heart:before { content: '\e088'; } /* '' */
|
||||||
|
.typcn-home-outline:before { content: '\e089'; } /* '' */
|
||||||
|
.typcn-home:before { content: '\e08a'; } /* '' */
|
||||||
|
.typcn-html5:before { content: '\e08b'; } /* '' */
|
||||||
|
.typcn-image-outline:before { content: '\e08c'; } /* '' */
|
||||||
|
.typcn-image:before { content: '\e08d'; } /* '' */
|
||||||
|
.typcn-infinity-outline:before { content: '\e08e'; } /* '' */
|
||||||
|
.typcn-infinity:before { content: '\e08f'; } /* '' */
|
||||||
|
.typcn-info-large-outline:before { content: '\e090'; } /* '' */
|
||||||
|
.typcn-info-large:before { content: '\e091'; } /* '' */
|
||||||
|
.typcn-info-outline:before { content: '\e092'; } /* '' */
|
||||||
|
.typcn-info:before { content: '\e093'; } /* '' */
|
||||||
|
.typcn-input-checked-outline:before { content: '\e094'; } /* '' */
|
||||||
|
.typcn-input-checked:before { content: '\e095'; } /* '' */
|
||||||
|
.typcn-key-outline:before { content: '\e096'; } /* '' */
|
||||||
|
.typcn-key:before { content: '\e097'; } /* '' */
|
||||||
|
.typcn-keyboard:before { content: '\e098'; } /* '' */
|
||||||
|
.typcn-leaf:before { content: '\e099'; } /* '' */
|
||||||
|
.typcn-lightbulb:before { content: '\e09a'; } /* '' */
|
||||||
|
.typcn-link-outline:before { content: '\e09b'; } /* '' */
|
||||||
|
.typcn-link:before { content: '\e09c'; } /* '' */
|
||||||
|
.typcn-location-arrow-outline:before { content: '\e09d'; } /* '' */
|
||||||
|
.typcn-location-arrow:before { content: '\e09e'; } /* '' */
|
||||||
|
.typcn-location-outline:before { content: '\e09f'; } /* '' */
|
||||||
|
.typcn-location:before { content: '\e0a0'; } /* '' */
|
||||||
|
.typcn-lock-closed-outline:before { content: '\e0a1'; } /* '' */
|
||||||
|
.typcn-lock-closed:before { content: '\e0a2'; } /* '' */
|
||||||
|
.typcn-lock-open-outline:before { content: '\e0a3'; } /* '' */
|
||||||
|
.typcn-lock-open:before { content: '\e0a4'; } /* '' */
|
||||||
|
.typcn-mail:before { content: '\e0a5'; } /* '' */
|
||||||
|
.typcn-map:before { content: '\e0a6'; } /* '' */
|
||||||
|
.typcn-media-eject-outline:before { content: '\e0a7'; } /* '' */
|
||||||
|
.typcn-media-eject:before { content: '\e0a8'; } /* '' */
|
||||||
|
.typcn-media-fast-forward-outline:before { content: '\e0a9'; } /* '' */
|
||||||
|
.typcn-media-fast-forward:before { content: '\e0aa'; } /* '' */
|
||||||
|
.typcn-media-pause-outline:before { content: '\e0ab'; } /* '' */
|
||||||
|
.typcn-media-pause:before { content: '\e0ac'; } /* '' */
|
||||||
|
.typcn-media-play-outline:before { content: '\e0ad'; } /* '' */
|
||||||
|
.typcn-media-play-reverse-outline:before { content: '\e0ae'; } /* '' */
|
||||||
|
.typcn-media-play-reverse:before { content: '\e0af'; } /* '' */
|
||||||
|
.typcn-media-play:before { content: '\e0b0'; } /* '' */
|
||||||
|
.typcn-media-record-outline:before { content: '\e0b1'; } /* '' */
|
||||||
|
.typcn-media-record:before { content: '\e0b2'; } /* '' */
|
||||||
|
.typcn-media-rewind-outline:before { content: '\e0b3'; } /* '' */
|
||||||
|
.typcn-media-rewind:before { content: '\e0b4'; } /* '' */
|
||||||
|
.typcn-media-stop-outline:before { content: '\e0b5'; } /* '' */
|
||||||
|
.typcn-media-stop:before { content: '\e0b6'; } /* '' */
|
||||||
|
.typcn-message-typing:before { content: '\e0b7'; } /* '' */
|
||||||
|
.typcn-message:before { content: '\e0b8'; } /* '' */
|
||||||
|
.typcn-messages:before { content: '\e0b9'; } /* '' */
|
||||||
|
.typcn-microphone-outline:before { content: '\e0ba'; } /* '' */
|
||||||
|
.typcn-microphone:before { content: '\e0bb'; } /* '' */
|
||||||
|
.typcn-minus-outline:before { content: '\e0bc'; } /* '' */
|
||||||
|
.typcn-minus:before { content: '\e0bd'; } /* '' */
|
||||||
|
.typcn-mortar-board:before { content: '\e0be'; } /* '' */
|
||||||
|
.typcn-news:before { content: '\e0bf'; } /* '' */
|
||||||
|
.typcn-notes-outline:before { content: '\e0c0'; } /* '' */
|
||||||
|
.typcn-notes:before { content: '\e0c1'; } /* '' */
|
||||||
|
.typcn-pen:before { content: '\e0c2'; } /* '' */
|
||||||
|
.typcn-pencil:before { content: '\e0c3'; } /* '' */
|
||||||
|
.typcn-phone-outline:before { content: '\e0c4'; } /* '' */
|
||||||
|
.typcn-phone:before { content: '\e0c5'; } /* '' */
|
||||||
|
.typcn-pi-outline:before { content: '\e0c6'; } /* '' */
|
||||||
|
.typcn-pi:before { content: '\e0c7'; } /* '' */
|
||||||
|
.typcn-pin-outline:before { content: '\e0c8'; } /* '' */
|
||||||
|
.typcn-pin:before { content: '\e0c9'; } /* '' */
|
||||||
|
.typcn-pipette:before { content: '\e0ca'; } /* '' */
|
||||||
|
.typcn-plane-outline:before { content: '\e0cb'; } /* '' */
|
||||||
|
.typcn-plane:before { content: '\e0cc'; } /* '' */
|
||||||
|
.typcn-plug:before { content: '\e0cd'; } /* '' */
|
||||||
|
.typcn-plus-outline:before { content: '\e0ce'; } /* '' */
|
||||||
|
.typcn-plus:before { content: '\e0cf'; } /* '' */
|
||||||
|
.typcn-point-of-interest-outline:before { content: '\e0d0'; } /* '' */
|
||||||
|
.typcn-point-of-interest:before { content: '\e0d1'; } /* '' */
|
||||||
|
.typcn-power-outline:before { content: '\e0d2'; } /* '' */
|
||||||
|
.typcn-power:before { content: '\e0d3'; } /* '' */
|
||||||
|
.typcn-printer:before { content: '\e0d4'; } /* '' */
|
||||||
|
.typcn-puzzle-outline:before { content: '\e0d5'; } /* '' */
|
||||||
|
.typcn-puzzle:before { content: '\e0d6'; } /* '' */
|
||||||
|
.typcn-radar-outline:before { content: '\e0d7'; } /* '' */
|
||||||
|
.typcn-radar:before { content: '\e0d8'; } /* '' */
|
||||||
|
.typcn-refresh-outline:before { content: '\e0d9'; } /* '' */
|
||||||
|
.typcn-refresh:before { content: '\e0da'; } /* '' */
|
||||||
|
.typcn-rss-outline:before { content: '\e0db'; } /* '' */
|
||||||
|
.typcn-rss:before { content: '\e0dc'; } /* '' */
|
||||||
|
.typcn-scissors-outline:before { content: '\e0dd'; } /* '' */
|
||||||
|
.typcn-scissors:before { content: '\e0de'; } /* '' */
|
||||||
|
.typcn-shopping-bag:before { content: '\e0df'; } /* '' */
|
||||||
|
.typcn-shopping-cart:before { content: '\e0e0'; } /* '' */
|
||||||
|
.typcn-social-at-circular:before { content: '\e0e1'; } /* '' */
|
||||||
|
.typcn-social-dribbble-circular:before { content: '\e0e2'; } /* '' */
|
||||||
|
.typcn-social-dribbble:before { content: '\e0e3'; } /* '' */
|
||||||
|
.typcn-social-facebook-circular:before { content: '\e0e4'; } /* '' */
|
||||||
|
.typcn-social-facebook:before { content: '\e0e5'; } /* '' */
|
||||||
|
.typcn-social-flickr-circular:before { content: '\e0e6'; } /* '' */
|
||||||
|
.typcn-social-flickr:before { content: '\e0e7'; } /* '' */
|
||||||
|
.typcn-social-github-circular:before { content: '\e0e8'; } /* '' */
|
||||||
|
.typcn-social-github:before { content: '\e0e9'; } /* '' */
|
||||||
|
.typcn-social-google-plus-circular:before { content: '\e0ea'; } /* '' */
|
||||||
|
.typcn-social-google-plus:before { content: '\e0eb'; } /* '' */
|
||||||
|
.typcn-social-instagram-circular:before { content: '\e0ec'; } /* '' */
|
||||||
|
.typcn-social-instagram:before { content: '\e0ed'; } /* '' */
|
||||||
|
.typcn-social-last-fm-circular:before { content: '\e0ee'; } /* '' */
|
||||||
|
.typcn-social-last-fm:before { content: '\e0ef'; } /* '' */
|
||||||
|
.typcn-social-linkedin-circular:before { content: '\e0f0'; } /* '' */
|
||||||
|
.typcn-social-linkedin:before { content: '\e0f1'; } /* '' */
|
||||||
|
.typcn-social-pinterest-circular:before { content: '\e0f2'; } /* '' */
|
||||||
|
.typcn-social-pinterest:before { content: '\e0f3'; } /* '' */
|
||||||
|
.typcn-social-skype-outline:before { content: '\e0f4'; } /* '' */
|
||||||
|
.typcn-social-skype:before { content: '\e0f5'; } /* '' */
|
||||||
|
.typcn-social-tumbler-circular:before { content: '\e0f6'; } /* '' */
|
||||||
|
.typcn-social-tumbler:before { content: '\e0f7'; } /* '' */
|
||||||
|
.typcn-social-twitter-circular:before { content: '\e0f8'; } /* '' */
|
||||||
|
.typcn-social-twitter:before { content: '\e0f9'; } /* '' */
|
||||||
|
.typcn-social-vimeo-circular:before { content: '\e0fa'; } /* '' */
|
||||||
|
.typcn-social-vimeo:before { content: '\e0fb'; } /* '' */
|
||||||
|
.typcn-social-youtube-circular:before { content: '\e0fc'; } /* '' */
|
||||||
|
.typcn-social-youtube:before { content: '\e0fd'; } /* '' */
|
||||||
|
.typcn-sort-alphabetically-outline:before { content: '\e0fe'; } /* '' */
|
||||||
|
.typcn-sort-alphabetically:before { content: '\e0ff'; } /* '' */
|
||||||
|
.typcn-sort-numerically-outline:before { content: '\e100'; } /* '' */
|
||||||
|
.typcn-sort-numerically:before { content: '\e101'; } /* '' */
|
||||||
|
.typcn-spanner-outline:before { content: '\e102'; } /* '' */
|
||||||
|
.typcn-spanner:before { content: '\e103'; } /* '' */
|
||||||
|
.typcn-spiral:before { content: '\e104'; } /* '' */
|
||||||
|
.typcn-star-full-outline:before { content: '\e105'; } /* '' */
|
||||||
|
.typcn-star-half-outline:before { content: '\e106'; } /* '' */
|
||||||
|
.typcn-star-half:before { content: '\e107'; } /* '' */
|
||||||
|
.typcn-star-outline:before { content: '\e108'; } /* '' */
|
||||||
|
.typcn-star:before { content: '\e109'; } /* '' */
|
||||||
|
.typcn-starburst-outline:before { content: '\e10a'; } /* '' */
|
||||||
|
.typcn-starburst:before { content: '\e10b'; } /* '' */
|
||||||
|
.typcn-stopwatch:before { content: '\e10c'; } /* '' */
|
||||||
|
.typcn-support:before { content: '\e10d'; } /* '' */
|
||||||
|
.typcn-tabs-outline:before { content: '\e10e'; } /* '' */
|
||||||
|
.typcn-tag:before { content: '\e10f'; } /* '' */
|
||||||
|
.typcn-tags:before { content: '\e110'; } /* '' */
|
||||||
|
.typcn-th-large-outline:before { content: '\e111'; } /* '' */
|
||||||
|
.typcn-th-large:before { content: '\e112'; } /* '' */
|
||||||
|
.typcn-th-list-outline:before { content: '\e113'; } /* '' */
|
||||||
|
.typcn-th-list:before { content: '\e114'; } /* '' */
|
||||||
|
.typcn-th-menu-outline:before { content: '\e115'; } /* '' */
|
||||||
|
.typcn-th-menu:before { content: '\e116'; } /* '' */
|
||||||
|
.typcn-th-small-outline:before { content: '\e117'; } /* '' */
|
||||||
|
.typcn-th-small:before { content: '\e118'; } /* '' */
|
||||||
|
.typcn-thermometer:before { content: '\e119'; } /* '' */
|
||||||
|
.typcn-thumbs-down:before { content: '\e11a'; } /* '' */
|
||||||
|
.typcn-thumbs-ok:before { content: '\e11b'; } /* '' */
|
||||||
|
.typcn-thumbs-up:before { content: '\e11c'; } /* '' */
|
||||||
|
.typcn-tick-outline:before { content: '\e11d'; } /* '' */
|
||||||
|
.typcn-tick:before { content: '\e11e'; } /* '' */
|
||||||
|
.typcn-ticket:before { content: '\e11f'; } /* '' */
|
||||||
|
.typcn-time:before { content: '\e120'; } /* '' */
|
||||||
|
.typcn-times-outline:before { content: '\e121'; } /* '' */
|
||||||
|
.typcn-times:before { content: '\e122'; } /* '' */
|
||||||
|
.typcn-trash:before { content: '\e123'; } /* '' */
|
||||||
|
.typcn-tree:before { content: '\e124'; } /* '' */
|
||||||
|
.typcn-upload-outline:before { content: '\e125'; } /* '' */
|
||||||
|
.typcn-upload:before { content: '\e126'; } /* '' */
|
||||||
|
.typcn-user-add-outline:before { content: '\e127'; } /* '' */
|
||||||
|
.typcn-user-add:before { content: '\e128'; } /* '' */
|
||||||
|
.typcn-user-delete-outline:before { content: '\e129'; } /* '' */
|
||||||
|
.typcn-user-delete:before { content: '\e12a'; } /* '' */
|
||||||
|
.typcn-user-outline:before { content: '\e12b'; } /* '' */
|
||||||
|
.typcn-user:before { content: '\e12c'; } /* '' */
|
||||||
|
.typcn-vendor-android:before { content: '\e12d'; } /* '' */
|
||||||
|
.typcn-vendor-apple:before { content: '\e12e'; } /* '' */
|
||||||
|
.typcn-vendor-microsoft:before { content: '\e12f'; } /* '' */
|
||||||
|
.typcn-video-outline:before { content: '\e130'; } /* '' */
|
||||||
|
.typcn-video:before { content: '\e131'; } /* '' */
|
||||||
|
.typcn-volume-down:before { content: '\e132'; } /* '' */
|
||||||
|
.typcn-volume-mute:before { content: '\e133'; } /* '' */
|
||||||
|
.typcn-volume-up:before { content: '\e134'; } /* '' */
|
||||||
|
.typcn-volume:before { content: '\e135'; } /* '' */
|
||||||
|
.typcn-warning-outline:before { content: '\e136'; } /* '' */
|
||||||
|
.typcn-warning:before { content: '\e137'; } /* '' */
|
||||||
|
.typcn-watch:before { content: '\e138'; } /* '' */
|
||||||
|
.typcn-waves-outline:before { content: '\e139'; } /* '' */
|
||||||
|
.typcn-waves:before { content: '\e13a'; } /* '' */
|
||||||
|
.typcn-weather-cloudy:before { content: '\e13b'; } /* '' */
|
||||||
|
.typcn-weather-downpour:before { content: '\e13c'; } /* '' */
|
||||||
|
.typcn-weather-night:before { content: '\e13d'; } /* '' */
|
||||||
|
.typcn-weather-partly-sunny:before { content: '\e13e'; } /* '' */
|
||||||
|
.typcn-weather-shower:before { content: '\e13f'; } /* '' */
|
||||||
|
.typcn-weather-snow:before { content: '\e140'; } /* '' */
|
||||||
|
.typcn-weather-stormy:before { content: '\e141'; } /* '' */
|
||||||
|
.typcn-weather-sunny:before { content: '\e142'; } /* '' */
|
||||||
|
.typcn-weather-windy-cloudy:before { content: '\e143'; } /* '' */
|
||||||
|
.typcn-weather-windy:before { content: '\e144'; } /* '' */
|
||||||
|
.typcn-wi-fi-outline:before { content: '\e145'; } /* '' */
|
||||||
|
.typcn-wi-fi:before { content: '\e146'; } /* '' */
|
||||||
|
.typcn-wine:before { content: '\e147'; } /* '' */
|
||||||
|
.typcn-world-outline:before { content: '\e148'; } /* '' */
|
||||||
|
.typcn-world:before { content: '\e149'; } /* '' */
|
||||||
|
.typcn-zoom-in-outline:before { content: '\e14a'; } /* '' */
|
||||||
|
.typcn-zoom-in:before { content: '\e14b'; } /* '' */
|
||||||
|
.typcn-zoom-out-outline:before { content: '\e14c'; } /* '' */
|
||||||
|
.typcn-zoom-out:before { content: '\e14d'; } /* '' */
|
||||||
|
.typcn-zoom-outline:before { content: '\e14e'; } /* '' */
|
||||||
|
.typcn-zoom:before { content: '\e14f'; } /* '' */</style></head><body><div class="page-header"><div class="container"><h1>Typicons<small> font demo</small></h1></div></div><div id="icons"><div id="preview" class="clearfix"><div data-name="adjust-brightness" data-code="0xe000" data-match="sun adjust brightness " class="icon"><span class="typcn typcn-adjust-brightness"></span></div><div data-name="adjust-contrast" data-code="0xe001" data-match="half adjust contrast " class="icon"><span class="typcn typcn-adjust-contrast"></span></div><div data-name="anchor-outline" data-code="0xe002" data-match=" anchor outline " class="icon"><span class="typcn typcn-anchor-outline"></span></div><div data-name="anchor" data-code="0xe003" data-match=" anchor " class="icon"><span class="typcn typcn-anchor"></span></div><div data-name="archive" data-code="0xe004" data-match=" archive " class="icon"><span class="typcn typcn-archive"></span></div><div data-name="arrow-back-outline" data-code="0xe005" data-match=" arrow back outline " class="icon"><span class="typcn typcn-arrow-back-outline"></span></div><div data-name="arrow-back" data-code="0xe006" data-match=" arrow back " class="icon"><span class="typcn typcn-arrow-back"></span></div><div data-name="arrow-down-outline" data-code="0xe007" data-match=" arrow down outline " class="icon"><span class="typcn typcn-arrow-down-outline"></span></div><div data-name="arrow-down-thick" data-code="0xe008" data-match=" arrow down thick " class="icon"><span class="typcn typcn-arrow-down-thick"></span></div><div data-name="arrow-down" data-code="0xe009" data-match=" arrow down " class="icon"><span class="typcn typcn-arrow-down"></span></div><div data-name="arrow-forward-outline" data-code="0xe00a" data-match=" arrow forward outline " class="icon"><span class="typcn typcn-arrow-forward-outline"></span></div><div data-name="arrow-forward" data-code="0xe00b" data-match=" arrow forward " class="icon"><span class="typcn typcn-arrow-forward"></span></div><div data-name="arrow-left-outline" data-code="0xe00c" data-match=" arrow left outline " class="icon"><span class="typcn typcn-arrow-left-outline"></span></div><div data-name="arrow-left-thick" data-code="0xe00d" data-match=" arrow left thick " class="icon"><span class="typcn typcn-arrow-left-thick"></span></div><div data-name="arrow-left" data-code="0xe00e" data-match=" arrow left " class="icon"><span class="typcn typcn-arrow-left"></span></div><div data-name="arrow-loop-outline" data-code="0xe00f" data-match=" arrow loop outline " class="icon"><span class="typcn typcn-arrow-loop-outline"></span></div><div data-name="arrow-loop" data-code="0xe010" data-match=" arrow loop " class="icon"><span class="typcn typcn-arrow-loop"></span></div><div data-name="arrow-maximise-outline" data-code="0xe011" data-match=" arrow maximise outline " class="icon"><span class="typcn typcn-arrow-maximise-outline"></span></div><div data-name="arrow-maximise" data-code="0xe012" data-match=" arrow maximise " class="icon"><span class="typcn typcn-arrow-maximise"></span></div><div data-name="arrow-minimise-outline" data-code="0xe013" data-match=" arrow minimise outline " class="icon"><span class="typcn typcn-arrow-minimise-outline"></span></div><div data-name="arrow-minimise" data-code="0xe014" data-match=" arrow minimise " class="icon"><span class="typcn typcn-arrow-minimise"></span></div><div data-name="arrow-move-outline" data-code="0xe015" data-match=" arrow move outline " class="icon"><span class="typcn typcn-arrow-move-outline"></span></div><div data-name="arrow-move" data-code="0xe016" data-match=" arrow move " class="icon"><span class="typcn typcn-arrow-move"></span></div><div data-name="arrow-repeat-outline" data-code="0xe017" data-match=" arrow repeat outline " class="icon"><span class="typcn typcn-arrow-repeat-outline"></span></div><div data-name="arrow-repeat" data-code="0xe018" data-match=" arrow repeat " class="icon"><span class="typcn typcn-arrow-repeat"></span></div><div data-name="arrow-right-outline" data-code="0xe019" data-match=" arrow right outline " class="icon"><span class="typcn typcn-arrow-right-outline"></span></div><div data-name="arrow-right-thick" data-code="0xe01a" data-match=" arrow right thick " class="icon"><span class="typcn typcn-arrow-right-thick"></span></div><div data-name="arrow-right" data-code="0xe01b" data-match=" arrow right " class="icon"><span class="typcn typcn-arrow-right"></span></div><div data-name="arrow-shuffle" data-code="0xe01c" data-match=" arrow shuffle " class="icon"><span class="typcn typcn-arrow-shuffle"></span></div><div data-name="arrow-sorted-down" data-code="0xe01d" data-match=" arrow sorted down " class="icon"><span class="typcn typcn-arrow-sorted-down"></span></div><div data-name="arrow-sorted-up" data-code="0xe01e" data-match=" arrow sorted up " class="icon"><span class="typcn typcn-arrow-sorted-up"></span></div><div data-name="arrow-sync-outline" data-code="0xe01f" data-match=" arrow sync outline " class="iLine truncated
|
||||||
File diff suppressed because it is too large.
Load diff
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
@charset 'UTF-8';@font-face{font-family:'typicons';font-weight:normal;font-style:normal;src:url('typicons.eot');src:url('typicons.eot?#iefix') format('embedded-opentype'),url('typicons.woff') format('woff'),url('typicons.ttf') format('truetype'),url('typicons.svg#typicons') format('svg')}.typcn:before{font-family:'typicons';font-style:normal;font-weight:normal;speak:none;display:inline-block;text-decoration:inherit;width:1em;height:1em;font-size:1em;text-align:center;-webkit-font-smoothing:antialiased;font-smoothing:antialiased;text-rendering:optimizeLegibility}.typcn-adjust-brightness:before{content:'\e000'}.typcn-adjust-contrast:before{content:'\e001'}.typcn-anchor-outline:before{content:'\e002'}.typcn-anchor:before{content:'\e003'}.typcn-archive:before{content:'\e004'}.typcn-arrow-back-outline:before{content:'\e005'}.typcn-arrow-back:before{content:'\e006'}.typcn-arrow-down-outline:before{content:'\e007'}.typcn-arrow-down-thick:before{content:'\e008'}.typcn-arrow-down:before{content:'\e009'}.typcn-arrow-forward-outline:before{content:'\e00a'}.typcn-arrow-forward:before{content:'\e00b'}.typcn-arrow-left-outline:before{content:'\e00c'}.typcn-arrow-left-thick:before{content:'\e00d'}.typcn-arrow-left:before{content:'\e00e'}.typcn-arrow-loop-outline:before{content:'\e00f'}.typcn-arrow-loop:before{content:'\e010'}.typcn-arrow-maximise-outline:before{content:'\e011'}.typcn-arrow-maximise:before{content:'\e012'}.typcn-arrow-minimise-outline:before{content:'\e013'}.typcn-arrow-minimise:before{content:'\e014'}.typcn-arrow-move-outline:before{content:'\e015'}.typcn-arrow-move:before{content:'\e016'}.typcn-arrow-repeat-outline:before{content:'\e017'}.typcn-arrow-repeat:before{content:'\e018'}.typcn-arrow-right-outline:before{content:'\e019'}.typcn-arrow-right-thick:before{content:'\e01a'}.typcn-arrow-right:before{content:'\e01b'}.typcn-arrow-shuffle:before{content:'\e01c'}.typcn-arrow-sorted-down:before{content:'\e01d'}.typcn-arrow-sorted-up:before{content:'\e01e'}.typcn-arrow-sync-outline:before{content:'\e01f'}.typcn-arrow-sync:before{content:'\e020'}.typcn-arrow-unsorted:before{content:'\e021'}.typcn-arrow-up-outline:before{content:'\e022'}.typcn-arrow-up-thick:before{content:'\e023'}.typcn-arrow-up:before{content:'\e024'}.typcn-at:before{content:'\e025'}.typcn-attachment-outline:before{content:'\e026'}.typcn-attachment:before{content:'\e027'}.typcn-backspace-outline:before{content:'\e028'}.typcn-backspace:before{content:'\e029'}.typcn-battery-charge:before{content:'\e02a'}.typcn-battery-full:before{content:'\e02b'}.typcn-battery-high:before{content:'\e02c'}.typcn-battery-low:before{content:'\e02d'}.typcn-battery-mid:before{content:'\e02e'}.typcn-beaker:before{content:'\e02f'}.typcn-beer:before{content:'\e030'}.typcn-bell:before{content:'\e031'}.typcn-book:before{content:'\e032'}.typcn-bookmark:before{content:'\e033'}.typcn-briefcase:before{content:'\e034'}.typcn-brush:before{content:'\e035'}.typcn-business-card:before{content:'\e036'}.typcn-calculator:before{content:'\e037'}.typcn-calendar-outline:before{content:'\e038'}.typcn-calendar:before{content:'\e039'}.typcn-camera-outline:before{content:'\e03a'}.typcn-camera:before{content:'\e03b'}.typcn-cancel-outline:before{content:'\e03c'}.typcn-cancel:before{content:'\e03d'}.typcn-chart-area-outline:before{content:'\e03e'}.typcn-chart-area:before{content:'\e03f'}.typcn-chart-bar-outline:before{content:'\e040'}.typcn-chart-bar:before{content:'\e041'}.typcn-chart-line-outline:before{content:'\e042'}.typcn-chart-line:before{content:'\e043'}.typcn-chart-pie-outline:before{content:'\e044'}.typcn-chart-pie:before{content:'\e045'}.typcn-chevron-left-outline:before{content:'\e046'}.typcn-chevron-left:before{content:'\e047'}.typcn-chevron-right-outline:before{content:'\e048'}.typcn-chevron-right:before{content:'\e049'}.typcn-clipboard:before{content:'\e04a'}.typcn-cloud-storage:before{content:'\e04b'}.typcn-cloud-storage-outline:before{content:'\e054'}.typcn-code-outline:before{content:'\e04c'}.typcn-code:before{content:'\e04d'}.typcn-coffee:before{content:'\e04e'}.typcn-cog-outline:before{content:'\e04f'}.typcn-cog:before{content:'\e050'}.typcn-compass:before{content:'\e051'}.typcn-contacts:before{content:'\e052'}.typcn-credit-card:before{content:'\e053'}.typcn-css3:before{content:'\e055'}.typcn-database:before{content:'\e056'}.typcn-delete-outline:before{content:'\e057'}.typcn-delete:before{content:'\e058'}.typcn-device-desktop:before{content:'\e059'}.typcn-device-laptop:before{content:'\e05a'}.typcn-device-phone:before{content:'\e05b'}.typcn-device-tablet:before{content:'\e05c'}.typcn-directions:before{content:'\e05d'}.typcn-divide-outline:before{content:'\e05e'}.typcn-divide:before{content:'\e05f'}.typcn-document-add:before{content:'\e060'}.typcn-document-delete:before{content:'\e061'}.typcn-document-text:before{content:'\e062'}.typcn-document:before{content:'\e063'}.typcn-download-outline:before{content:'\e064'}.typcn-download:before{content:'\e065'}.typcn-dropbox:beforLine truncated
|
||||||
File diff suppressed because it is too large.
Load diff
|
After Width: | Height: | Size: 182 KiB |
Binary file not shown.
Binary file not shown.
@@ -21,18 +21,18 @@
|
|||||||
<div class="btn-toolbar" role="toolbar" aria-label="Subscriptions toolbar">
|
<div class="btn-toolbar" role="toolbar" aria-label="Subscriptions toolbar">
|
||||||
<div class="btn-group btn-group-sm mr-2" role="group">
|
<div class="btn-group btn-group-sm mr-2" role="group">
|
||||||
<button id="btn_create_sub" type="button" class="btn btn-secondary" >
|
<button id="btn_create_sub" type="button" class="btn btn-secondary" >
|
||||||
<i class="material-icons" aria-hidden="true">add</i>
|
<span class="typcn typcn-plus" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
<button id="btn_create_folder" type="button" class="btn btn-secondary">
|
<button id="btn_create_folder" type="button" class="btn btn-secondary">
|
||||||
<i class="material-icons" aria-hidden="true">create_new_folder</i>
|
<span class="typcn typcn-folder-add" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group btn-group-sm mr-2" role="group">
|
<div class="btn-group btn-group-sm mr-2" role="group">
|
||||||
<button id="btn_edit_node" type="button" class="btn btn-secondary" >
|
<button id="btn_edit_node" type="button" class="btn btn-secondary" >
|
||||||
<i class="material-icons" aria-hidden="true">edit</i>
|
<span class="typcn typcn-edit" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
<button id="btn_delete_node" type="button" class="btn btn-secondary" >
|
<button id="btn_delete_node" type="button" class="btn btn-secondary" >
|
||||||
<i class="material-icons" aria-hidden="true">delete</i>
|
<span class="typcn typcn-trash" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -41,4 +41,10 @@
|
|||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block detail %}
|
||||||
|
<div id="main_detail">
|
||||||
|
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -279,10 +279,10 @@ function tree_Initialize()
|
|||||||
},
|
},
|
||||||
types : {
|
types : {
|
||||||
folder : {
|
folder : {
|
||||||
icon : "material-icons material-folder"
|
icon : "typcn typcn-folder"
|
||||||
},
|
},
|
||||||
sub : {
|
sub : {
|
||||||
icon : "material-icons material-person",
|
icon : "typcn typcn-user",
|
||||||
max_depth : 0
|
max_depth : 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -314,6 +314,13 @@ function tree_ValidateChange(operation, node, parent, position, more)
|
|||||||
function tree_OnSelectionChanged(e, data)
|
function tree_OnSelectionChanged(e, data)
|
||||||
{
|
{
|
||||||
node = data.instance.get_selected(true)[0];
|
node = data.instance.get_selected(true)[0];
|
||||||
|
$.post("{% url 'ajax_list_videos' %}", {
|
||||||
|
type: node.type,
|
||||||
|
id: node.id.replace('folder', '').replace('sub', ''),
|
||||||
|
csrfmiddlewaretoken: '{{ csrf_token }}'
|
||||||
|
}).done(function (result) {
|
||||||
|
$("#main_detail").html(result);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<title>{% block title %}YouTube Subscription Manager{% endblock %}</title>
|
<title>{% block title %}YouTube Subscription Manager{% endblock %}</title>
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
|
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
<link rel="stylesheet" href="{% static 'YtManagerApp/import/typicons/typicons.min.css' %}" />
|
||||||
<link rel="stylesheet" href="{% static 'YtManagerApp/css/style.css' %}">
|
<link rel="stylesheet" href="{% static 'YtManagerApp/css/style.css' %}">
|
||||||
{% block stylesheets %}
|
{% block stylesheets %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<div class="video-gallery container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
{% for video in videos %}
|
||||||
|
<div class="card-wrapper col-12 col-sm-6 col-lg-4 col-xl-3 d-flex align-items-stretch">
|
||||||
|
<div class="card mx-auto">
|
||||||
|
<img class="card-img-top" src="{{ video.icon_best }}" alt="Thumbnail">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">
|
||||||
|
{% if not video.watched %}
|
||||||
|
<sup class="badge badge-primary">New</sup>
|
||||||
|
{% endif %}
|
||||||
|
{{ video.name }}
|
||||||
|
</h5>
|
||||||
|
<p class="card-text">{{ video.description | truncatechars:120 }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer dropdown show">
|
||||||
|
<span class="typcn typcn-eye {{ video.watched | yesno:"video-icon-yes,video-icon-no" }}"
|
||||||
|
title="{{ video.watched | yesno:"Watched,Not watched" }}"></span>
|
||||||
|
<span class="typcn typcn-download {{ video.downloaded_path | yesno:"video-icon-yes,,video-icon-no" }}"
|
||||||
|
title="{{ video.downloaded_path | yesno:"Downloaded,,Not downloaded" }}"></span>
|
||||||
|
<small class="text-muted">{{ video.publish_date }}</small>
|
||||||
|
<a class="card-more float-right text-muted"
|
||||||
|
href="#" role="button" id="dropdownMenuLink"
|
||||||
|
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><span class="typcn typcn-cog"></span></a>
|
||||||
|
<div class="dropdown-menu" aria-labelledby="dropdownMenuLink">
|
||||||
|
<a class="dropdown-item" href="#">Mark {{ video.watched | yesno:"not watched,watched" }}</a>
|
||||||
|
{% if video.downloaded_path %}
|
||||||
|
<a class="dropdown-item" href="#">Delete downloaded</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="dropdown-item" href="#">Download</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,114 @@
|
|||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
import re
|
||||||
|
from configparser import Interpolation, NoSectionError, NoOptionError, InterpolationMissingOptionError, \
|
||||||
|
InterpolationDepthError, InterpolationSyntaxError, ConfigParser
|
||||||
|
|
||||||
|
MAX_INTERPOLATION_DEPTH = 10
|
||||||
|
|
||||||
|
|
||||||
|
class ExtendedInterpolatorWithEnv(Interpolation):
|
||||||
|
"""Advanced variant of interpolation, supports the syntax used by
|
||||||
|
`zc.buildout'. Enables interpolation between sections.
|
||||||
|
|
||||||
|
This modified version also allows specifying environment variables
|
||||||
|
using ${env:...}, and allows adding additional options using 'set_additional_options'. """
|
||||||
|
|
||||||
|
_KEYCRE = re.compile(r"\$\{([^}]+)\}")
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.__kwargs = kwargs
|
||||||
|
|
||||||
|
def set_additional_options(self, **kwargs):
|
||||||
|
self.__kwargs = kwargs
|
||||||
|
|
||||||
|
def before_get(self, parser, section, option, value, defaults):
|
||||||
|
L = []
|
||||||
|
self._interpolate_some(parser, option, L, value, section, defaults, 1)
|
||||||
|
return ''.join(L)
|
||||||
|
|
||||||
|
def before_set(self, parser, section, option, value):
|
||||||
|
tmp_value = value.replace('$$', '') # escaped dollar signs
|
||||||
|
tmp_value = self._KEYCRE.sub('', tmp_value) # valid syntax
|
||||||
|
if '$' in tmp_value:
|
||||||
|
raise ValueError("invalid interpolation syntax in %r at "
|
||||||
|
"position %d" % (value, tmp_value.find('$')))
|
||||||
|
return value
|
||||||
|
|
||||||
|
def _resolve_option(self, option, defaults):
|
||||||
|
if option in self.__kwargs:
|
||||||
|
return self.__kwargs[option]
|
||||||
|
return defaults[option]
|
||||||
|
|
||||||
|
def _resolve_section_option(self, section, option, parser):
|
||||||
|
if section == 'env':
|
||||||
|
return os.getenv(option, '')
|
||||||
|
return parser.get(section, option, raw=True)
|
||||||
|
|
||||||
|
def _interpolate_some(self, parser, option, accum, rest, section, map,
|
||||||
|
depth):
|
||||||
|
rawval = parser.get(section, option, raw=True, fallback=rest)
|
||||||
|
if depth > MAX_INTERPOLATION_DEPTH:
|
||||||
|
raise InterpolationDepthError(option, section, rawval)
|
||||||
|
while rest:
|
||||||
|
p = rest.find("$")
|
||||||
|
if p < 0:
|
||||||
|
accum.append(rest)
|
||||||
|
return
|
||||||
|
if p > 0:
|
||||||
|
accum.append(rest[:p])
|
||||||
|
rest = rest[p:]
|
||||||
|
# p is no longer used
|
||||||
|
c = rest[1:2]
|
||||||
|
if c == "$":
|
||||||
|
accum.append("$")
|
||||||
|
rest = rest[2:]
|
||||||
|
elif c == "{":
|
||||||
|
m = self._KEYCRE.match(rest)
|
||||||
|
if m is None:
|
||||||
|
raise InterpolationSyntaxError(option, section,
|
||||||
|
"bad interpolation variable reference %r" % rest)
|
||||||
|
path = m.group(1).split(':')
|
||||||
|
rest = rest[m.end():]
|
||||||
|
sect = section
|
||||||
|
opt = option
|
||||||
|
try:
|
||||||
|
if len(path) == 1:
|
||||||
|
opt = parser.optionxform(path[0])
|
||||||
|
v = self._resolve_option(opt, map)
|
||||||
|
elif len(path) == 2:
|
||||||
|
sect = path[0]
|
||||||
|
opt = parser.optionxform(path[1])
|
||||||
|
v = self._resolve_section_option(sect, opt, parser)
|
||||||
|
else:
|
||||||
|
raise InterpolationSyntaxError(
|
||||||
|
option, section,
|
||||||
|
"More than one ':' found: %r" % (rest,))
|
||||||
|
except (KeyError, NoSectionError, NoOptionError):
|
||||||
|
raise InterpolationMissingOptionError(
|
||||||
|
option, section, rawval, ":".join(path)) from None
|
||||||
|
if "$" in v:
|
||||||
|
self._interpolate_some(parser, opt, accum, v, sect,
|
||||||
|
dict(parser.items(sect, raw=True)),
|
||||||
|
depth + 1)
|
||||||
|
else:
|
||||||
|
accum.append(v)
|
||||||
|
else:
|
||||||
|
raise InterpolationSyntaxError(
|
||||||
|
option, section,
|
||||||
|
"'$' must be followed by '$' or '{', "
|
||||||
|
"found: %r" % (rest,))
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigParserWithEnv(ConfigParser):
|
||||||
|
_DEFAULT_INTERPOLATION = ExtendedInterpolatorWithEnv()
|
||||||
|
|
||||||
|
def set_additional_interpolation_options(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Sets additional options to be used in interpolation.
|
||||||
|
Only works with ExtendedInterpolatorWithEnv
|
||||||
|
:param kwargs:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if isinstance(super()._interpolation, ExtendedInterpolatorWithEnv):
|
||||||
|
super()._interpolation.set_additional_options(**kwargs)
|
||||||
File renamed without changes.
+17
-1
@@ -20,7 +20,8 @@ def get_children_recurse(parent_id):
|
|||||||
children.append({
|
children.append({
|
||||||
"id": "sub" + str(sub.id),
|
"id": "sub" + str(sub.id),
|
||||||
"type": "sub",
|
"type": "sub",
|
||||||
"text": sub.name
|
"text": sub.name,
|
||||||
|
"icon": sub.icon_default
|
||||||
})
|
})
|
||||||
|
|
||||||
return children
|
return children
|
||||||
@@ -82,6 +83,21 @@ def ajax_delete_subscription(request: HttpRequest, sid):
|
|||||||
return HttpResponse()
|
return HttpResponse()
|
||||||
|
|
||||||
|
|
||||||
|
def ajax_list_videos(request: HttpRequest):
|
||||||
|
if request.method == 'POST':
|
||||||
|
type = request.POST['type']
|
||||||
|
id = request.POST['id']
|
||||||
|
context = {}
|
||||||
|
|
||||||
|
if type == 'sub':
|
||||||
|
context['videos'] = SubscriptionManager.list_videos(int(id))
|
||||||
|
else:
|
||||||
|
context['videos'] = FolderManager.list_videos(int(id))
|
||||||
|
|
||||||
|
return render(request, 'YtManagerApp/main_videos.html', context)
|
||||||
|
|
||||||
|
|
||||||
def index(request: HttpRequest):
|
def index(request: HttpRequest):
|
||||||
context = {}
|
context = {}
|
||||||
return render(request, 'YtManagerApp/index.html', context)
|
return render(request, 'YtManagerApp/index.html', context)
|
||||||
|
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
; Use $<env:environment_variable> to use the value of an environment variable.
|
||||||
|
; The global section contains settings that apply to the entire server
|
||||||
|
[global]
|
||||||
|
; YouTube API key - get this from your user account
|
||||||
|
YoutubeApiKey=AIzaSyBabzE4Bup77WexdLMa9rN9z-wJidEfNX8
|
||||||
|
|
||||||
|
; Specifies the synchronization schedule, in crontab format.
|
||||||
|
; Format: <minute> <hour> <day-of-month> <month-of-year> <year>
|
||||||
|
SynchronizationSchedule=0 * * * * *
|
||||||
|
|
||||||
|
; Number of threads running the scheduler
|
||||||
|
; Since most of the jobs scheduled are downloads, there is no advantage to having
|
||||||
|
; a higher concurrency
|
||||||
|
SchedulerConcurrency=2
|
||||||
|
|
||||||
|
; Log level
|
||||||
|
LogLevel=DEBUG
|
||||||
|
|
||||||
|
; Default user settings
|
||||||
|
[user]
|
||||||
|
; When a video is deleted on the system, it will be marked as 'watched'
|
||||||
|
MarkDeletedAsWatched=True
|
||||||
|
|
||||||
|
; Videos marked as watched are automatically deleted
|
||||||
|
DeleteWatched=True
|
||||||
|
|
||||||
|
; Enable automatic downloading
|
||||||
|
AutoDownload=True
|
||||||
|
|
||||||
|
; Limit the total number of videos downloaded (-1 or empty = no limit)
|
||||||
|
DownloadGlobalLimit=
|
||||||
|
|
||||||
|
; Limit the numbers of videos per subscription (-1 or empty = no limit)
|
||||||
|
DownloadSubscriptionLimit=5
|
||||||
|
|
||||||
|
; Number of download attempts
|
||||||
|
DownloadMaxAttempts=3
|
||||||
|
|
||||||
|
; Download order
|
||||||
|
; Options: playlist_index, publish_date, name.
|
||||||
|
; Use - to reverse order (e.g. -publish_date means to order by publish date descending)
|
||||||
|
DownloadOrder=playlist_index
|
||||||
|
|
||||||
|
; Path where downloaded videos are stored
|
||||||
|
;DownloadPath=${env:USERPROFILE}${env:HOME}/Downloads
|
||||||
|
DownloadPath=D:\\Dev\\youtube-channel-manager\\temp\\download
|
||||||
|
|
||||||
|
; A pattern which describes how downloaded files are organized. Extensions are automatically appended.
|
||||||
|
; Supported fields: channel, channel_id, playlist, playlist_id, playlist_index, title, id
|
||||||
|
; The default pattern should work pretty well with Plex
|
||||||
|
DownloadFilePattern=${channel}/${playlist}/S01E${playlist_index} - ${title} [${id}]
|
||||||
|
|
||||||
|
; Download format that will be passed to youtube-dl. See the youtube-dl documentation for more details.
|
||||||
|
DownloadFormat=bestvideo+bestaudio
|
||||||
|
|
||||||
|
; Subtitles - these options match the youtube-dl options
|
||||||
|
DownloadSubtitles=True
|
||||||
|
DownloadAutogeneratedSubtitles=False
|
||||||
|
DownloadSubtitlesAll=False
|
||||||
|
DownloadSubtitlesLangs=en,ro
|
||||||
|
DownloadSubtitlesFormat=
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
; Use $<env:environment_variable> to use the value of an environment variable.
|
||||||
|
; The global section contains settings that apply to the entire server
|
||||||
|
[global]
|
||||||
|
; YouTube API key - get this from your user account
|
||||||
|
YoutubeApiKey=AIzaSyBabzE4Bup77WexdLMa9rN9z-wJidEfNX8
|
||||||
|
|
||||||
|
; Specifies the synchronization schedule, in crontab format.
|
||||||
|
; Format: <minute> <hour> <day-of-month> <month-of-year> <year>
|
||||||
|
SynchronizationSchedule=0 * * * * *
|
||||||
|
|
||||||
|
; Number of threads running the scheduler
|
||||||
|
; Since most of the jobs scheduled are downloads, there is no advantage to having
|
||||||
|
; a higher concurrency
|
||||||
|
SchedulerConcurrency=2
|
||||||
|
|
||||||
|
; Log level
|
||||||
|
LogLevel=DEBUG
|
||||||
|
|
||||||
|
; Default user settings
|
||||||
|
[user]
|
||||||
|
; When a video is deleted on the system, it will be marked as 'watched'
|
||||||
|
MarkDeletedAsWatched=True
|
||||||
|
|
||||||
|
; Videos marked as watched are automatically deleted
|
||||||
|
DeleteWatched=True
|
||||||
|
|
||||||
|
; Enable automatic downloading
|
||||||
|
AutoDownload=True
|
||||||
|
|
||||||
|
; Limit the total number of videos downloaded (-1 or empty = no limit)
|
||||||
|
DownloadGlobalLimit=
|
||||||
|
|
||||||
|
; Limit the numbers of videos per subscription (-1 or empty = no limit)
|
||||||
|
DownloadSubscriptionLimit=5
|
||||||
|
|
||||||
|
; Number of download attempts
|
||||||
|
DownloadMaxAttempts=3
|
||||||
|
|
||||||
|
; Download order
|
||||||
|
; Options: playlist_index, publish_date, name.
|
||||||
|
; Use - to reverse order (e.g. -publish_date means to order by publish date descending)
|
||||||
|
DownloadOrder=playlist_index
|
||||||
|
|
||||||
|
; Path where downloaded videos are stored
|
||||||
|
;DownloadPath=${env:USERPROFILE}${env:HOME}/Downloads
|
||||||
|
DownloadPath=D:\\Dev\\youtube-channel-manager\\temp\\download
|
||||||
|
|
||||||
|
; A pattern which describes how downloaded files are organized. Extensions are automatically appended.
|
||||||
|
; Supported fields: channel, channel_id, playlist, playlist_id, playlist_index, title, id
|
||||||
|
; The default pattern should work pretty well with Plex
|
||||||
|
DownloadFilePattern=${channel}/${playlist}/S01E${playlist_index} - ${title} [${id}]
|
||||||
|
|
||||||
|
; Download format that will be passed to youtube-dl. See the youtube-dl documentation for more details.
|
||||||
|
DownloadFormat=bestvideo+bestaudio
|
||||||
|
|
||||||
|
; Subtitles - these options match the youtube-dl options
|
||||||
|
DownloadSubtitles=True
|
||||||
|
DownloadAutogeneratedSubtitles=False
|
||||||
|
DownloadSubtitlesAll=False
|
||||||
|
DownloadSubtitlesLangs=en,ro
|
||||||
|
DownloadSubtitlesFormat=
|
||||||
Reference in new issue
Block a user