diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..d770e59 --- /dev/null +++ b/.babelrc @@ -0,0 +1,16 @@ +{ + // need loose mode for IE9 https://phabricator.babeljs.io/T3041 + "presets": [ + [ + "es2015", + { + "loose": true + } + ], + "stage-1", + "react" + ], + "plugins": [ + "transform-decorators-legacy" + ] +} diff --git a/.bootstraprc b/.bootstraprc new file mode 100644 index 0000000..195aedb --- /dev/null +++ b/.bootstraprc @@ -0,0 +1,117 @@ +--- +# Output debugging info +# loglevel: debug + +# Major version of Bootstrap: 3 or 4 +bootstrapVersion: 3 + +# If Bootstrap version 3 is used - turn on/off custom icon font path +useCustomIconFontPath: false + +# Webpack loaders, order matters +styleLoaders: + - style-loader + - css-loader + - sass-loader + +# Extract styles to stand-alone css file +# Different settings for different environments can be used, +# It depends on value of NODE_ENV environment variable +# This param can also be set in webpack config: +# entry: 'bootstrap-loader/extractStyles' +# extractStyles: false +extractStyles: true +# env: +# development: +# extractStyles: false +# production: +# extractStyles: true + + +# Customize Bootstrap variables that get imported before the original Bootstrap variables. +# Thus, derived Bootstrap variables can depend on values from here. +# See the Bootstrap _variables.scss file for examples of derived Bootstrap variables. +# +# preBootstrapCustomizations: ./path/to/bootstrap/pre-customizations.scss + + +# This gets loaded after bootstrap/variables is loaded +# Thus, you may customize Bootstrap variables +# based on the values established in the Bootstrap _variables.scss file +# +bootstrapCustomizations: ./app/l8pr/static/styles/config/_variables.scss + + +# Import your custom styles here +# Usually this endpoint-file contains list of @imports of your application styles +# +# appStyles: ./path/to/your/app/styles/endpoint.scss + + +### Bootstrap styles +styles: + + # Mixins + mixins: true + + # Reset and dependencies + normalize: true + print: true + glyphicons: true + + # Core CSS + scaffolding: true + type: true + code: true + grid: true + tables: true + forms: true + buttons: true + + # Components + component-animations: true + dropdowns: true + button-groups: true + input-groups: true + navs: true + navbar: true + breadcrumbs: true + pagination: true + pager: true + labels: true + badges: true + jumbotron: true + thumbnails: true + alerts: true + progress-bars: true + media: true + list-group: true + panels: true + wells: true + responsive-embed: true + close: true + + # Components w/ JavaScript + modals: true + tooltip: true + popovers: true + carousel: true + + # Utility classes + utilities: true + responsive-utilities: true + +### Bootstrap scripts +scripts: + transition: true + alert: true + button: true + carousel: true + collapse: true + dropdown: true + modal: true + tooltip: true + popover: true + scrollspy: true + tab: true + affix: true diff --git a/.bowerrc b/.bowerrc deleted file mode 100644 index 0666e13..0000000 --- a/.bowerrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "directory": "app/l8pr/static/bower_components" -} diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..2eaa40c --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,73 @@ +module.exports = { + env: { + browser: true, + commonjs: true, + es6: true, + }, + parser: 'babel-eslint', + extends: ['eslint:recommended'], + globals: { + $: false, + $$: false, + _: false, + angular: false, + browser: false, + by: false, + element: false, + gettext: false, + inject: false, + protractor: false, + requirejs: false + }, + parserOptions: { + ecmaFeatures: { + experimentalObjectRestSpread: true, + jsx: true + }, + sourceType: 'module' + }, + plugins: [ + 'react', + ], + rules: { + 'react/jsx-uses-react': 'error', + 'react/react-in-jsx-scope': 'error', + 'react/jsx-uses-vars': 'error', + 'react/jsx-no-undef': 'error', + 'react/prop-types': 2, + 'react/no-children-prop': 2, + 'react/no-did-update-set-state': 2, + 'react/no-direct-mutation-state': 2, + 'react/no-multi-comp': 2, + 'react/no-render-return-value': 2, + 'react/no-unknown-property': 2, + 'react/no-unused-prop-types': 2, + 'react/self-closing-comp': 2, + 'react/prefer-es6-class': 2, + 'no-trailing-spaces': 'error', + 'object-property-newline': 'error', + 'object-curly-newline': 'error', + 'comma-dangle': ['error', 'always-multiline'], + 'object-curly-spacing':['error', 'always'], + 'no-console': 'error', + 'block-scoped-var': 2, + complexity: [2, 10], + indent: [ + 'error', + 4, + { SwitchCase: 1 } + ], + 'linebreak-style': [ + 'error', + 'unix' + ], + quotes: [ + 'error', + 'single' + ], + semi: [ + 'error', + 'never' + ] + } +} diff --git a/.gitignore b/.gitignore index 06b6ec0..ff9a939 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ app/l8pr/static/CACHE app/l8pr/static/bower_components staticfiles/ uploaded/ +static_dist/ +yarn.lock diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 8aebada..0000000 --- a/.travis.yml +++ /dev/null @@ -1,31 +0,0 @@ -sudo: required - -services: - - docker - -env: - DEBUG: 'False' - global: - secure: ZGp7iIf8Eq+6D0exKy2s+/QAjP2DE1qCRSIDRJ8JNm+OZwjgfdWPw87UPbGq+aKhUtbjWfUwpHcXk/t1eYrjEe4f44SzflCGlTgA33LAiL7EWmwjuUe8oDyaiuEAOlxTSb0lQwe27LQrTqmo/aoO5noGZR8PqTL2gqcut7TNKjUXqK6VGovUlimS8BHN4bZmcVtoCWP9UmEsCErpEaxtpxx2rOEHJgW+NXsYN72+s1G59OxVIhuMgVcfGU+EMqRpgcuC+NDiKH5farWrlQbdy5h9dBr06q5/ljcBsnFYfYlPuMYI4ud4H8P5Gcc+VWsYINXJlWbazihsoQJxPbwp36hlQPye/+bdIT3fbqFl4qNB3iIm964Y35w8r+PvVL2PlUTyW9F5jvj61BOVmkpABneh9S8gXXMD5tUbur8SUEfpWPcMY212CLA/16IUPabvzf5C/Od5Wlvc4p2jN8ZmJxhmbWfVka+P9Tp01vZWNeVMWHuEw4cYRt5epE1MQcBhJ6WriHgl/i554goUS+TBq19SzV7S2PuVtTVOJADYIE1gw9y4dH7z23H8ynMEtDsJSHhIhXzRklldC6AbD8kzbk6YXtvgr30DdZT5ytkY7fYa7fsrVeZQlpwlhGjAeQmdy6XQ6iM2SzF8hpIOBQTPB159mmkoI+Uo/m5cGVbb/K4= - -before_install: - - sudo sh -c 'echo "deb https://apt.dockerproject.org/repo ubuntu-precise main" > /etc/apt/sources.list.d/docker.list' - - sudo apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D - - sudo apt-get update - - sudo apt-key update - - sudo apt-get -qqy --force-yes -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" install docker-engine=1.11.1-0~precise - - sudo rm /usr/local/bin/docker-compose - - curl -L https://github.com/docker/compose/releases/download/1.7.1/docker-compose-`uname -s`-`uname -m` > docker-compose - - chmod +x docker-compose - - sudo mv docker-compose /usr/local/bin - - docker-compose -v - - docker -v - -script: - - docker-compose -f docker-compose-travis.yml build - - docker-compose -f docker-compose-travis.yml up -d - - sleep 10 - - docker-compose -f docker-compose-travis.yml run web python3 manage.py migrate --settings app.settings_docker - - docker-compose -f docker-compose-travis.yml run web python3 manage.py loaddata tests --settings app.settings_docker - - docker-compose -f docker-compose-travis.yml run web bash -c 'export DISPLAY=:99.0 && /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1920x1080x24; protractor protractor.js --baseUrl=http://web:8000' - - docker-compose -f docker-compose-travis.yml logs diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 5de435f..0000000 --- a/Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -FROM python:3.4.3 -ENV PYTHONUNBUFFERED 1 -RUN mkdir /code -WORKDIR /code - -RUN apt-get update || true -RUN curl -sL https://deb.nodesource.com/setup_4.x | bash - -RUN apt-get install -y nodejs -RUN apt-get install -y openjdk-7-jdk nodejs chromium Xvfb - -ADD requirements.txt /code/ -RUN pip install -r requirements.txt - -COPY ./package.json /code/ -COPY ./bower.json /code/ -COPY ./.bowerrc /code/ - -RUN npm install -g npm -RUN npm install -g protractor -RUN webdriver-manager update -RUN npm install -g bower -RUN npm install -RUN bower --allow-root install -ADD . /code/ diff --git a/app/l8pr/admin.py b/app/l8pr/admin.py index a1994ef..e9789e0 100644 --- a/app/l8pr/admin.py +++ b/app/l8pr/admin.py @@ -24,6 +24,7 @@ class LoopAdmin(admin.ModelAdmin): list_display = ('__str__', 'active') inlines = (ShowsRelationshipInline,) + admin.site.register(Loop, LoopAdmin) @@ -31,21 +32,28 @@ class ShowAdmin(admin.ModelAdmin): list_display = ('__str__', 'user', 'added', 'updated') inlines = (SettingsInline, ItemsRelationshipInline,) + admin.site.register(Show, ShowAdmin) class ItemAdmin(admin.ModelAdmin): list_display = ('__str__', 'title', 'author_name', 'provider_name', 'duration') + + admin.site.register(Item, ItemAdmin) class ShowsRelationshipAdmin(admin.ModelAdmin): pass + + admin.site.register(ShowsRelationship, ShowsRelationshipAdmin) class ItemsRelationshipAdmin(admin.ModelAdmin): - pass + search_fields = ['item__title', 'show__title', 'item__id', 'show__id'] + + admin.site.register(ItemsRelationship, ItemsRelationshipAdmin) diff --git a/app/l8pr/api.py b/app/l8pr/api.py index 6b77e93..c5ca142 100644 --- a/app/l8pr/api.py +++ b/app/l8pr/api.py @@ -1,22 +1,32 @@ from django.contrib.auth.models import User from .models import Loop, Show, Item, ShowSettings, Profile, ItemsRelationship, get_metadata, ShowsRelationship from rest_framework import serializers, viewsets, views -from rest_framework.decorators import list_route +from rest_framework.decorators import list_route, detail_route from rest_framework.response import Response from django.shortcuts import get_object_or_404 from rest_framework import status -from drf_haystack.serializers import HaystackSerializer +from drf_haystack.serializers import HaystackSerializerMixin, HaystackSerializer from drf_haystack.viewsets import HaystackViewSet -from .search_indexes import ItemIndex, ShowIndex +from .search_indexes import ItemIndex, ShowIndex, UserIndex from .youtube import youtube_search from django.utils import timezone +from .permissions import IsOwnerOrReadOnly -class ItemSerializer(serializers.ModelSerializer): - id = serializers.IntegerField(required=False) +class ItemsField(serializers.Field): + def get_attribute(self, obj): + # We pass the object instance onto `to_representation`, + # not just the field attribute. + return obj - class Meta: - model = Item + def to_representation(self, obj): + items = [rel.item for rel in ItemsRelationship.objects.filter(show=obj).order_by('-order')] + return ItemSerializer(items, many=True, read_only=True).data + + def to_internal_value(self, data): + serializer = ItemSerializer(data=data, many=True) + serializer.is_valid(raise_exception=True) + return serializer.validated_data class ShowSettingsSerializer(serializers.ModelSerializer): @@ -25,25 +35,29 @@ class Meta: fields = ('shuffle', 'dj_layout', 'giphy', 'force_giphy', 'giphy_tags', 'hide_strip') -class ProfileSerializer(serializers.ModelSerializer): +class SimpleUserProfileSerializer(serializers.ModelSerializer): + id = serializers.IntegerField(source='user.id', required=True) + username = serializers.CharField(source='user.username', required=True) + loops = serializers.PrimaryKeyRelatedField(source='user.loops', many=True, read_only=True) + class Meta: model = Profile + fields = ('id', 'username', 'loops') -class ItemsField(serializers.Field): - def get_attribute(self, obj): - # We pass the object instance onto `to_representation`, - # not just the field attribute. - return obj +class ProfileSerializer(serializers.ModelSerializer): + follows = SimpleUserProfileSerializer(many=True, required=False, read_only=True) + followers = SimpleUserProfileSerializer(many=True, required=False, read_only=True) - def to_representation(self, obj): - items = [rel.item for rel in ItemsRelationship.objects.filter(show=obj).order_by('-order')] - return ItemSerializer(items, many=True, read_only=True).data + class Meta: + fields = '__all__' + model = Profile - def to_internal_value(self, data): - serializer = ItemSerializer(data=data, many=True) - serializer.is_valid(raise_exception=True) - return serializer.validated_data + +class SimpleUserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ('id', 'username') class UserSerializer(serializers.ModelSerializer): @@ -54,19 +68,23 @@ class Meta: fields = ('id', 'username', 'loops', 'profile') +class ShowNameSerializer(serializers.ModelSerializer): + user = SimpleUserSerializer() + + class Meta: + model = Show + fields = ('id', 'title', 'user', 'show_type') + + class ShowSerializer(serializers.ModelSerializer): items = ItemsField() settings = ShowSettingsSerializer(required=False) user = UserSerializer(required=False, read_only=True) - username = serializers.SerializerMethodField(read_only=True) class Meta: model = Show fields = ('id', 'added', 'updated', 'show_type', 'title', 'description', - 'items', 'user', 'settings', 'username') - - def get_username(self, obj): - return obj.user.username + 'items', 'user', 'settings') def create(self, validated_data): validated_data.pop('settings', {}) @@ -116,8 +134,21 @@ def update(self, instance, validated_data): return instance +class BaseItemSerializer(serializers.ModelSerializer): + id = serializers.IntegerField(required=False) + + class Meta: + fields = '__all__' + model = Item + + +class ItemSerializer(BaseItemSerializer): + shows = ShowNameSerializer(many=True, required=False) + + class LoopSerializer(serializers.ModelSerializer): shows_list = serializers.SerializerMethodField() + user = UserSerializer() def get_current_user(self): user = None @@ -151,7 +182,19 @@ def getByUsername(self, request, username): @list_route(methods=['get'], url_path='me') def me(self, request): if (request.user.is_anonymous()): - return Response('nope', status=status.HTTP_401_UNAUTHORIZED) + return Response(status=status.HTTP_401_UNAUTHORIZED) + return Response(UserSerializer(request.user, context={'request': request}).data, status=status.HTTP_200_OK) + + @detail_route(methods=['post'], permission_classes=[IsOwnerOrReadOnly]) + def follow(self, request, pk=None): + profile = get_object_or_404(Profile, user=pk) + request.user.profile.follows.add(profile) + return Response(UserSerializer(request.user, context={'request': request}).data, status=status.HTTP_200_OK) + + @detail_route(methods=['post'], permission_classes=[IsOwnerOrReadOnly]) + def unfollow(self, request, pk=None): + profile = get_object_or_404(Profile, user=pk) + request.user.profile.follows.remove(profile) return Response(UserSerializer(request.user, context={'request': request}).data, status=status.HTTP_200_OK) @@ -176,34 +219,64 @@ class ShowViewSet(viewsets.ModelViewSet): class ItemViewSet(viewsets.ModelViewSet): - queryset = Item.objects.all() + queryset = Item.objects.distinct() + serializer_class = ItemSerializer + filter_fields = ('url', 'shows__user__username') + + +class FeedView(viewsets.ReadOnlyModelViewSet): + queryset = Item.objects.distinct() serializer_class = ItemSerializer - filter_fields = ('url',) + filter_fields = ('__all__') + def list(self, request): + following = [p.user.id for p in request.user.profile.follows.all()] + items = Item.objects \ + .filter(shows__user__in=following, shows__show_type='normal') \ + .order_by('-ItemsRelationship__updated') + page = self.paginate_queryset(items) + if page is not None: + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + serializer = self.get_serializer(items, many=True) + return Response(serializer.data) -class ItemSearchSerializer(HaystackSerializer): - id = serializers.CharField() +class ItemSearchSerializer(HaystackSerializerMixin, ItemSerializer): + class Meta(ItemSerializer.Meta): + search_fields = ('text', 'title',) + field_aliases = {} + exclude = [] + + +class ShowSearchSerializer(HaystackSerializerMixin, ShowSerializer): + class Meta(ShowSerializer.Meta): + search_fields = ('text', 'title',) + field_aliases = {} + exclude = [] + + +class UserSearchSerializer(HaystackSerializerMixin, UserSerializer): + class Meta(UserSerializer.Meta): + search_fields = ('text', 'username',) + field_aliases = {} + exclude = [] + + +class AggregateSearchSerializer(HaystackSerializer): class Meta: - # The `index_classes` attribute is a list of which search indexes - # we want to include in the search. - index_classes = [ItemIndex, ShowIndex] - # The `fields` contains all the fields we want to include. - # NOTE: Make sure you don't confuse these with model attributes. These - # fields belong to the search index! - fields = [ - 'title', 'author_name', 'autocomplete', 'thumbnail', 'id', 'url', 'provider_name' - ] + serializers = { + ItemIndex: ItemSearchSerializer, + ShowIndex: ShowSearchSerializer, + UserIndex: UserSearchSerializer, + } class ItemSearchView(HaystackViewSet): - - # `index_models` is an optional list of which models you would like to include - # in the search result. You might have several models indexed, and this provides - # a way to filter out those of no interest for this particular view. - # (Translates to `SearchQuerySet().models(*index_models)` behind the scenes. + permission_classes = [] + serializer_class = AggregateSearchSerializer + # to remove in order to have more than Items in results index_models = [Item, Show] - serializer_class = ItemSearchSerializer class SearchYoutubeView(views.APIView): @@ -211,7 +284,7 @@ class SearchYoutubeView(views.APIView): def get(self, request, *args, **kw): result = youtube_search({'q': request.GET.get('q')}) - return Response(ItemSerializer(result, many=True).data, status=status.HTTP_200_OK) + return Response(BaseItemSerializer(result, many=True).data, status=status.HTTP_200_OK) class MetadataView(views.APIView): @@ -222,4 +295,4 @@ def get(self, request, *args, **kw): result = Item.objects.filter(url=url).first() if not result: result = get_metadata(url) - return Response(ItemSerializer(result, many=False).data, status=status.HTTP_200_OK) + return Response(BaseItemSerializer(result, many=False).data, status=status.HTTP_200_OK) diff --git a/app/l8pr/fixtures/groups.json b/app/l8pr/fixtures/groups.json deleted file mode 100644 index 6381294..0000000 --- a/app/l8pr/fixtures/groups.json +++ /dev/null @@ -1 +0,0 @@ -[{"model": "auth.group", "pk": 1, "fields": {"name": "api user", "permissions": [58, 59, 60, 61, 62, 63, 47, 43, 44, 45, 55, 56, 57, 52, 53, 54, 49, 50, 51]}}] \ No newline at end of file diff --git a/app/l8pr/fixtures/tests.json b/app/l8pr/fixtures/tests.json deleted file mode 100644 index 18b1179..0000000 --- a/app/l8pr/fixtures/tests.json +++ /dev/null @@ -1,3258 +0,0 @@ -[ - { - "fields": { - "codename": "add_logentry", - "content_type": 1, - "name": "Can add log entry" - }, - "model": "auth.permission", - "pk": 1 - }, - { - "fields": { - "codename": "change_logentry", - "content_type": 1, - "name": "Can change log entry" - }, - "model": "auth.permission", - "pk": 2 - }, - { - "fields": { - "codename": "delete_logentry", - "content_type": 1, - "name": "Can delete log entry" - }, - "model": "auth.permission", - "pk": 3 - }, - { - "fields": { - "codename": "add_permission", - "content_type": 2, - "name": "Can add permission" - }, - "model": "auth.permission", - "pk": 4 - }, - { - "fields": { - "codename": "change_permission", - "content_type": 2, - "name": "Can change permission" - }, - "model": "auth.permission", - "pk": 5 - }, - { - "fields": { - "codename": "delete_permission", - "content_type": 2, - "name": "Can delete permission" - }, - "model": "auth.permission", - "pk": 6 - }, - { - "fields": { - "codename": "add_group", - "content_type": 3, - "name": "Can add group" - }, - "model": "auth.permission", - "pk": 7 - }, - { - "fields": { - "codename": "change_group", - "content_type": 3, - "name": "Can change group" - }, - "model": "auth.permission", - "pk": 8 - }, - { - "fields": { - "codename": "delete_group", - "content_type": 3, - "name": "Can delete group" - }, - "model": "auth.permission", - "pk": 9 - }, - { - "fields": { - "codename": "add_user", - "content_type": 4, - "name": "Can add user" - }, - "model": "auth.permission", - "pk": 10 - }, - { - "fields": { - "codename": "change_user", - "content_type": 4, - "name": "Can change user" - }, - "model": "auth.permission", - "pk": 11 - }, - { - "fields": { - "codename": "delete_user", - "content_type": 4, - "name": "Can delete user" - }, - "model": "auth.permission", - "pk": 12 - }, - { - "fields": { - "codename": "add_contenttype", - "content_type": 5, - "name": "Can add content type" - }, - "model": "auth.permission", - "pk": 13 - }, - { - "fields": { - "codename": "change_contenttype", - "content_type": 5, - "name": "Can change content type" - }, - "model": "auth.permission", - "pk": 14 - }, - { - "fields": { - "codename": "delete_contenttype", - "content_type": 5, - "name": "Can delete content type" - }, - "model": "auth.permission", - "pk": 15 - }, - { - "fields": { - "codename": "add_session", - "content_type": 6, - "name": "Can add session" - }, - "model": "auth.permission", - "pk": 16 - }, - { - "fields": { - "codename": "change_session", - "content_type": 6, - "name": "Can change session" - }, - "model": "auth.permission", - "pk": 17 - }, - { - "fields": { - "codename": "delete_session", - "content_type": 6, - "name": "Can delete session" - }, - "model": "auth.permission", - "pk": 18 - }, - { - "fields": { - "codename": "add_application", - "content_type": 7, - "name": "Can add application" - }, - "model": "auth.permission", - "pk": 19 - }, - { - "fields": { - "codename": "change_application", - "content_type": 7, - "name": "Can change application" - }, - "model": "auth.permission", - "pk": 20 - }, - { - "fields": { - "codename": "delete_application", - "content_type": 7, - "name": "Can delete application" - }, - "model": "auth.permission", - "pk": 21 - }, - { - "fields": { - "codename": "add_grant", - "content_type": 8, - "name": "Can add grant" - }, - "model": "auth.permission", - "pk": 22 - }, - { - "fields": { - "codename": "change_grant", - "content_type": 8, - "name": "Can change grant" - }, - "model": "auth.permission", - "pk": 23 - }, - { - "fields": { - "codename": "delete_grant", - "content_type": 8, - "name": "Can delete grant" - }, - "model": "auth.permission", - "pk": 24 - }, - { - "fields": { - "codename": "add_accesstoken", - "content_type": 9, - "name": "Can add access token" - }, - "model": "auth.permission", - "pk": 25 - }, - { - "fields": { - "codename": "change_accesstoken", - "content_type": 9, - "name": "Can change access token" - }, - "model": "auth.permission", - "pk": 26 - }, - { - "fields": { - "codename": "delete_accesstoken", - "content_type": 9, - "name": "Can delete access token" - }, - "model": "auth.permission", - "pk": 27 - }, - { - "fields": { - "codename": "add_refreshtoken", - "content_type": 10, - "name": "Can add refresh token" - }, - "model": "auth.permission", - "pk": 28 - }, - { - "fields": { - "codename": "change_refreshtoken", - "content_type": 10, - "name": "Can change refresh token" - }, - "model": "auth.permission", - "pk": 29 - }, - { - "fields": { - "codename": "delete_refreshtoken", - "content_type": 10, - "name": "Can delete refresh token" - }, - "model": "auth.permission", - "pk": 30 - }, - { - "fields": { - "codename": "add_usersocialauth", - "content_type": 11, - "name": "Can add user social auth" - }, - "model": "auth.permission", - "pk": 31 - }, - { - "fields": { - "codename": "change_usersocialauth", - "content_type": 11, - "name": "Can change user social auth" - }, - "model": "auth.permission", - "pk": 32 - }, - { - "fields": { - "codename": "delete_usersocialauth", - "content_type": 11, - "name": "Can delete user social auth" - }, - "model": "auth.permission", - "pk": 33 - }, - { - "fields": { - "codename": "add_nonce", - "content_type": 12, - "name": "Can add nonce" - }, - "model": "auth.permission", - "pk": 34 - }, - { - "fields": { - "codename": "change_nonce", - "content_type": 12, - "name": "Can change nonce" - }, - "model": "auth.permission", - "pk": 35 - }, - { - "fields": { - "codename": "delete_nonce", - "content_type": 12, - "name": "Can delete nonce" - }, - "model": "auth.permission", - "pk": 36 - }, - { - "fields": { - "codename": "add_association", - "content_type": 13, - "name": "Can add association" - }, - "model": "auth.permission", - "pk": 37 - }, - { - "fields": { - "codename": "change_association", - "content_type": 13, - "name": "Can change association" - }, - "model": "auth.permission", - "pk": 38 - }, - { - "fields": { - "codename": "delete_association", - "content_type": 13, - "name": "Can delete association" - }, - "model": "auth.permission", - "pk": 39 - }, - { - "fields": { - "codename": "add_code", - "content_type": 14, - "name": "Can add code" - }, - "model": "auth.permission", - "pk": 40 - }, - { - "fields": { - "codename": "change_code", - "content_type": 14, - "name": "Can change code" - }, - "model": "auth.permission", - "pk": 41 - }, - { - "fields": { - "codename": "delete_code", - "content_type": 14, - "name": "Can delete code" - }, - "model": "auth.permission", - "pk": 42 - }, - { - "fields": { - "codename": "add_profile", - "content_type": 15, - "name": "Can add profile" - }, - "model": "auth.permission", - "pk": 43 - }, - { - "fields": { - "codename": "change_profile", - "content_type": 15, - "name": "Can change profile" - }, - "model": "auth.permission", - "pk": 44 - }, - { - "fields": { - "codename": "delete_profile", - "content_type": 15, - "name": "Can delete profile" - }, - "model": "auth.permission", - "pk": 45 - }, - { - "fields": { - "codename": "add_loop", - "content_type": 16, - "name": "Can add loop" - }, - "model": "auth.permission", - "pk": 46 - }, - { - "fields": { - "codename": "change_loop", - "content_type": 16, - "name": "Can change loop" - }, - "model": "auth.permission", - "pk": 47 - }, - { - "fields": { - "codename": "delete_loop", - "content_type": 16, - "name": "Can delete loop" - }, - "model": "auth.permission", - "pk": 48 - }, - { - "fields": { - "codename": "add_showsrelationship", - "content_type": 17, - "name": "Can add shows relationship" - }, - "model": "auth.permission", - "pk": 49 - }, - { - "fields": { - "codename": "change_showsrelationship", - "content_type": 17, - "name": "Can change shows relationship" - }, - "model": "auth.permission", - "pk": 50 - }, - { - "fields": { - "codename": "delete_showsrelationship", - "content_type": 17, - "name": "Can delete shows relationship" - }, - "model": "auth.permission", - "pk": 51 - }, - { - "fields": { - "codename": "add_showsettings", - "content_type": 18, - "name": "Can add show settings" - }, - "model": "auth.permission", - "pk": 52 - }, - { - "fields": { - "codename": "change_showsettings", - "content_type": 18, - "name": "Can change show settings" - }, - "model": "auth.permission", - "pk": 53 - }, - { - "fields": { - "codename": "delete_showsettings", - "content_type": 18, - "name": "Can delete show settings" - }, - "model": "auth.permission", - "pk": 54 - }, - { - "fields": { - "codename": "add_show", - "content_type": 19, - "name": "Can add show" - }, - "model": "auth.permission", - "pk": 55 - }, - { - "fields": { - "codename": "change_show", - "content_type": 19, - "name": "Can change show" - }, - "model": "auth.permission", - "pk": 56 - }, - { - "fields": { - "codename": "delete_show", - "content_type": 19, - "name": "Can delete show" - }, - "model": "auth.permission", - "pk": 57 - }, - { - "fields": { - "codename": "add_itemsrelationship", - "content_type": 20, - "name": "Can add items relationship" - }, - "model": "auth.permission", - "pk": 58 - }, - { - "fields": { - "codename": "change_itemsrelationship", - "content_type": 20, - "name": "Can change items relationship" - }, - "model": "auth.permission", - "pk": 59 - }, - { - "fields": { - "codename": "delete_itemsrelationship", - "content_type": 20, - "name": "Can delete items relationship" - }, - "model": "auth.permission", - "pk": 60 - }, - { - "fields": { - "codename": "add_item", - "content_type": 21, - "name": "Can add item" - }, - "model": "auth.permission", - "pk": 61 - }, - { - "fields": { - "codename": "change_item", - "content_type": 21, - "name": "Can change item" - }, - "model": "auth.permission", - "pk": 62 - }, - { - "fields": { - "codename": "delete_item", - "content_type": 21, - "name": "Can delete item" - }, - "model": "auth.permission", - "pk": 63 - }, - { - "fields": { - "name": "api user", - "permissions": [ - 61, - 62, - 63, - 58, - 59, - 60, - 47, - 43, - 44, - 45, - 55, - 56, - 57, - 52, - 53, - 54, - 49, - 50, - 51 - ] - }, - "model": "auth.group", - "pk": 1 - }, - { - "fields": { - "date_joined": "2016-06-28T21:15:14.790Z", - "email": "vied12@gmail.com", - "first_name": "", - "groups": [ - 1 - ], - "is_active": true, - "is_staff": true, - "is_superuser": true, - "last_login": "2016-07-06T09:11:54.371Z", - "last_name": "", - "password": "pbkdf2_sha256$24000$Sy3MtMPtsC1l$t/x03imd4SBxtArqaRNPzgT102hKTUfQZsPu3WI/bGU=", - "user_permissions": [], - "username": "vied12" - }, - "model": "auth.user", - "pk": 1 - }, - { - "fields": { - "date_joined": "2016-06-28T21:15:18.763Z", - "email": "benjamin.etco@gmail.com", - "first_name": "", - "groups": [ - 1 - ], - "is_active": true, - "is_staff": true, - "is_superuser": true, - "last_login": null, - "last_name": "", - "password": "pbkdf2_sha256$24000$Fh1fOd55D8A5$NVnPHGM759cfdWzs2MoGQ0VzI6q9mARPiktNy0Bi3XE=", - "user_permissions": [], - "username": "discover" - }, - "model": "auth.user", - "pk": 2 - }, - { - "fields": { - "active": true, - "added": "2016-06-28T21:15:14.833Z", - "updated": "2016-06-28T21:15:14.833Z", - "user": 1 - }, - "model": "l8pr.loop", - "pk": 1 - }, - { - "fields": { - "active": true, - "added": "2016-06-28T21:15:18.807Z", - "updated": "2016-06-28T21:15:18.807Z", - "user": 2 - }, - "model": "l8pr.loop", - "pk": 2 - }, - { - "fields": { - "loop": 1, - "order": 1, - "show": 2 - }, - "model": "l8pr.showsrelationship", - "pk": 2 - }, - { - "fields": { - "loop": 1, - "order": 2, - "show": 3 - }, - "model": "l8pr.showsrelationship", - "pk": 3 - }, - { - "fields": { - "loop": 1, - "order": 9, - "show": 10 - }, - "model": "l8pr.showsrelationship", - "pk": 10 - }, - { - "fields": { - "loop": 1, - "order": 10, - "show": 11 - }, - "model": "l8pr.showsrelationship", - "pk": 11 - }, - { - "fields": { - "loop": 2, - "order": 1, - "show": 19 - }, - "model": "l8pr.showsrelationship", - "pk": 19 - }, - { - "fields": { - "loop": 2, - "order": 3, - "show": 21 - }, - "model": "l8pr.showsrelationship", - "pk": 21 - }, - { - "fields": { - "loop": 2, - "order": 7, - "show": 25 - }, - "model": "l8pr.showsrelationship", - "pk": 25 - }, - { - "fields": { - "loop": 2, - "order": 10, - "show": 28 - }, - "model": "l8pr.showsrelationship", - "pk": 28 - }, - { - "fields": { - "dj_layout": false, - "force_giphy": false, - "giphy": false, - "giphy_tags": null, - "hide_strip": false, - "show": 2, - "shuffle": false - }, - "model": "l8pr.showsettings", - "pk": 2 - }, - { - "fields": { - "dj_layout": false, - "force_giphy": false, - "giphy": false, - "giphy_tags": null, - "hide_strip": false, - "show": 3, - "shuffle": false - }, - "model": "l8pr.showsettings", - "pk": 3 - }, - { - "fields": { - "dj_layout": false, - "force_giphy": false, - "giphy": false, - "giphy_tags": null, - "hide_strip": false, - "show": 10, - "shuffle": true - }, - "model": "l8pr.showsettings", - "pk": 10 - }, - { - "fields": { - "dj_layout": true, - "force_giphy": false, - "giphy": false, - "giphy_tags": "psychedelic,punch,rave,electro", - "hide_strip": false, - "show": 11, - "shuffle": true - }, - "model": "l8pr.showsettings", - "pk": 11 - }, - { - "fields": { - "dj_layout": false, - "force_giphy": false, - "giphy": false, - "giphy_tags": null, - "hide_strip": false, - "show": 19, - "shuffle": false - }, - "model": "l8pr.showsettings", - "pk": 19 - }, - { - "fields": { - "dj_layout": true, - "force_giphy": false, - "giphy": false, - "giphy_tags": null, - "hide_strip": true, - "show": 21, - "shuffle": true - }, - "model": "l8pr.showsettings", - "pk": 21 - }, - { - "fields": { - "dj_layout": false, - "force_giphy": false, - "giphy": false, - "giphy_tags": "dance,sunglasses,abstract", - "hide_strip": false, - "show": 25, - "shuffle": false - }, - "model": "l8pr.showsettings", - "pk": 25 - }, - { - "fields": { - "dj_layout": true, - "force_giphy": false, - "giphy": false, - "giphy_tags": "serge-gainsbourg,jane-birkin", - "hide_strip": false, - "show": 28, - "shuffle": true - }, - "model": "l8pr.showsettings", - "pk": 28 - }, - { - "fields": { - "added": "2016-06-28T21:15:15.104Z", - "description": null, - "title": "Trailers", - "updated": "2016-06-28T21:15:15.104Z", - "user": 1 - }, - "model": "l8pr.show", - "pk": 2 - }, - { - "fields": { - "added": "2016-06-28T21:15:15.163Z", - "description": null, - "title": "Live Music", - "updated": "2016-06-28T21:15:15.163Z", - "user": 1 - }, - "model": "l8pr.show", - "pk": 3 - }, - { - "fields": { - "added": "2016-06-28T21:15:16.430Z", - "description": null, - "title": "Entertainment", - "updated": "2016-06-28T21:15:16.431Z", - "user": 1 - }, - "model": "l8pr.show", - "pk": 10 - }, - { - "fields": { - "added": "2016-06-28T21:15:16.642Z", - "description": null, - "title": "SLWX", - "updated": "2016-06-28T21:15:16.642Z", - "user": 1 - }, - "model": "l8pr.show", - "pk": 11 - }, - { - "fields": { - "added": "2016-06-28T21:15:19.591Z", - "description": null, - "title": "Docu", - "updated": "2016-06-28T21:15:19.592Z", - "user": 2 - }, - "model": "l8pr.show", - "pk": 19 - }, - { - "fields": { - "added": "2016-06-28T21:15:27.301Z", - "description": null, - "title": "Beau", - "updated": "2016-06-28T21:15:27.301Z", - "user": 2 - }, - "model": "l8pr.show", - "pk": 21 - }, - { - "fields": { - "added": "2016-06-28T21:15:30.430Z", - "description": null, - "title": "TV", - "updated": "2016-06-28T21:15:30.431Z", - "user": 2 - }, - "model": "l8pr.show", - "pk": 25 - }, - { - "fields": { - "added": "2016-06-28T21:15:30.430Z", - "description": null, - "title": "my inbox", - "show_type": "inbox", - "updated": "2016-06-28T21:15:30.431Z", - "user": 2 - }, - "model": "l8pr.show", - "pk": 29 - }, - { - "fields": { - "added": "2016-06-28T21:15:33.938Z", - "description": null, - "title": "GainsB.", - "updated": "2016-06-28T21:15:33.938Z", - "user": 2 - }, - "model": "l8pr.show", - "pk": 28 - }, - { - "fields": { - "item": 7, - "order": 0, - "show": 2 - }, - "model": "l8pr.itemsrelationship", - "pk": 7 - }, - { - "fields": { - "item": 8, - "order": 0, - "show": 3 - }, - "model": "l8pr.itemsrelationship", - "pk": 8 - }, - { - "fields": { - "item": 9, - "order": 1, - "show": 3 - }, - "model": "l8pr.itemsrelationship", - "pk": 9 - }, - { - "fields": { - "item": 10, - "order": 2, - "show": 3 - }, - "model": "l8pr.itemsrelationship", - "pk": 10 - }, - { - "fields": { - "item": 11, - "order": 3, - "show": 3 - }, - "model": "l8pr.itemsrelationship", - "pk": 11 - }, - { - "fields": { - "item": 12, - "order": 4, - "show": 3 - }, - "model": "l8pr.itemsrelationship", - "pk": 12 - }, - { - "fields": { - "item": 32, - "order": 0, - "show": 10 - }, - "model": "l8pr.itemsrelationship", - "pk": 32 - }, - { - "fields": { - "item": 33, - "order": 1, - "show": 10 - }, - "model": "l8pr.itemsrelationship", - "pk": 33 - }, - { - "fields": { - "item": 34, - "order": 2, - "show": 10 - }, - "model": "l8pr.itemsrelationship", - "pk": 34 - }, - { - "fields": { - "item": 35, - "order": 3, - "show": 10 - }, - "model": "l8pr.itemsrelationship", - "pk": 35 - }, - { - "fields": { - "item": 36, - "order": 4, - "show": 10 - }, - "model": "l8pr.itemsrelationship", - "pk": 36 - }, - { - "fields": { - "item": 37, - "order": 5, - "show": 10 - }, - "model": "l8pr.itemsrelationship", - "pk": 37 - }, - { - "fields": { - "item": 38, - "order": 0, - "show": 11 - }, - "model": "l8pr.itemsrelationship", - "pk": 38 - }, - { - "fields": { - "item": 39, - "order": 1, - "show": 11 - }, - "model": "l8pr.itemsrelationship", - "pk": 39 - }, - { - "fields": { - "item": 40, - "order": 2, - "show": 11 - }, - "model": "l8pr.itemsrelationship", - "pk": 40 - }, - { - "fields": { - "item": 41, - "order": 3, - "show": 11 - }, - "model": "l8pr.itemsrelationship", - "pk": 41 - }, - { - "fields": { - "item": 42, - "order": 4, - "show": 11 - }, - "model": "l8pr.itemsrelationship", - "pk": 42 - }, - { - "fields": { - "item": 43, - "order": 5, - "show": 11 - }, - "model": "l8pr.itemsrelationship", - "pk": 43 - }, - { - "fields": { - "item": 44, - "order": 6, - "show": 11 - }, - "model": "l8pr.itemsrelationship", - "pk": 44 - }, - { - "fields": { - "item": 45, - "order": 7, - "show": 11 - }, - "model": "l8pr.itemsrelationship", - "pk": 45 - }, - { - "fields": { - "item": 46, - "order": 8, - "show": 11 - }, - "model": "l8pr.itemsrelationship", - "pk": 46 - }, - { - "fields": { - "item": 91, - "order": 0, - "show": 19 - }, - "model": "l8pr.itemsrelationship", - "pk": 91 - }, - { - "fields": { - "item": 92, - "order": 1, - "show": 19 - }, - "model": "l8pr.itemsrelationship", - "pk": 92 - }, - { - "fields": { - "item": 93, - "order": 2, - "show": 19 - }, - "model": "l8pr.itemsrelationship", - "pk": 93 - }, - { - "fields": { - "item": 94, - "order": 3, - "show": 19 - }, - "model": "l8pr.itemsrelationship", - "pk": 94 - }, - { - "fields": { - "item": 95, - "order": 4, - "show": 19 - }, - "model": "l8pr.itemsrelationship", - "pk": 95 - }, - { - "fields": { - "item": 96, - "order": 5, - "show": 19 - }, - "model": "l8pr.itemsrelationship", - "pk": 96 - }, - { - "fields": { - "item": 97, - "order": 6, - "show": 19 - }, - "model": "l8pr.itemsrelationship", - "pk": 97 - }, - { - "fields": { - "item": 98, - "order": 7, - "show": 19 - }, - "model": "l8pr.itemsrelationship", - "pk": 98 - }, - { - "fields": { - "item": 99, - "order": 8, - "show": 19 - }, - "model": "l8pr.itemsrelationship", - "pk": 99 - }, - { - "fields": { - "item": 100, - "order": 9, - "show": 19 - }, - "model": "l8pr.itemsrelationship", - "pk": 100 - }, - { - "fields": { - "item": 101, - "order": 10, - "show": 19 - }, - "model": "l8pr.itemsrelationship", - "pk": 101 - }, - { - "fields": { - "item": 102, - "order": 11, - "show": 19 - }, - "model": "l8pr.itemsrelationship", - "pk": 102 - }, - { - "fields": { - "item": 103, - "order": 12, - "show": 19 - }, - "model": "l8pr.itemsrelationship", - "pk": 103 - }, - { - "fields": { - "item": 104, - "order": 13, - "show": 19 - }, - "model": "l8pr.itemsrelationship", - "pk": 104 - }, - { - "fields": { - "item": 105, - "order": 14, - "show": 19 - }, - "model": "l8pr.itemsrelationship", - "pk": 105 - }, - { - "fields": { - "item": 106, - "order": 15, - "show": 19 - }, - "model": "l8pr.itemsrelationship", - "pk": 106 - }, - { - "fields": { - "item": 107, - "order": 16, - "show": 19 - }, - "model": "l8pr.itemsrelationship", - "pk": 107 - }, - { - "fields": { - "item": 108, - "order": 17, - "show": 19 - }, - "model": "l8pr.itemsrelationship", - "pk": 108 - }, - { - "fields": { - "item": 109, - "order": 18, - "show": 19 - }, - "model": "l8pr.itemsrelationship", - "pk": 109 - }, - { - "fields": { - "item": 110, - "order": 19, - "show": 19 - }, - "model": "l8pr.itemsrelationship", - "pk": 110 - }, - { - "fields": { - "item": 111, - "order": 20, - "show": 19 - }, - "model": "l8pr.itemsrelationship", - "pk": 111 - }, - { - "fields": { - "item": 112, - "order": 21, - "show": 19 - }, - "model": "l8pr.itemsrelationship", - "pk": 112 - }, - { - "fields": { - "item": 113, - "order": 22, - "show": 19 - }, - "model": "l8pr.itemsrelationship", - "pk": 113 - }, - { - "fields": { - "item": 114, - "order": 23, - "show": 19 - }, - "model": "l8pr.itemsrelationship", - "pk": 114 - }, - { - "fields": { - "item": 115, - "order": 24, - "show": 19 - }, - "model": "l8pr.itemsrelationship", - "pk": 115 - }, - { - "fields": { - "item": 116, - "order": 25, - "show": 19 - }, - "model": "l8pr.itemsrelationship", - "pk": 116 - }, - { - "fields": { - "item": 117, - "order": 26, - "show": 19 - }, - "model": "l8pr.itemsrelationship", - "pk": 117 - }, - { - "fields": { - "item": 118, - "order": 27, - "show": 19 - }, - "model": "l8pr.itemsrelationship", - "pk": 118 - }, - { - "fields": { - "item": 119, - "order": 28, - "show": 19 - }, - "model": "l8pr.itemsrelationship", - "pk": 119 - }, - { - "fields": { - "item": 120, - "order": 29, - "show": 19 - }, - "model": "l8pr.itemsrelationship", - "pk": 120 - }, - { - "fields": { - "item": 121, - "order": 30, - "show": 19 - }, - "model": "l8pr.itemsrelationship", - "pk": 121 - }, - { - "fields": { - "item": 122, - "order": 31, - "show": 19 - }, - "model": "l8pr.itemsrelationship", - "pk": 122 - }, - { - "fields": { - "item": 252, - "order": 0, - "show": 21 - }, - "model": "l8pr.itemsrelationship", - "pk": 252 - }, - { - "fields": { - "item": 253, - "order": 1, - "show": 21 - }, - "model": "l8pr.itemsrelationship", - "pk": 253 - }, - { - "fields": { - "item": 254, - "order": 2, - "show": 21 - }, - "model": "l8pr.itemsrelationship", - "pk": 254 - }, - { - "fields": { - "item": 255, - "order": 3, - "show": 21 - }, - "model": "l8pr.itemsrelationship", - "pk": 255 - }, - { - "fields": { - "item": 256, - "order": 4, - "show": 21 - }, - "model": "l8pr.itemsrelationship", - "pk": 256 - }, - { - "fields": { - "item": 257, - "order": 5, - "show": 21 - }, - "model": "l8pr.itemsrelationship", - "pk": 257 - }, - { - "fields": { - "item": 258, - "order": 6, - "show": 21 - }, - "model": "l8pr.itemsrelationship", - "pk": 258 - }, - { - "fields": { - "item": 259, - "order": 7, - "show": 21 - }, - "model": "l8pr.itemsrelationship", - "pk": 259 - }, - { - "fields": { - "item": 260, - "order": 8, - "show": 21 - }, - "model": "l8pr.itemsrelationship", - "pk": 260 - }, - { - "fields": { - "item": 261, - "order": 9, - "show": 21 - }, - "model": "l8pr.itemsrelationship", - "pk": 261 - }, - { - "fields": { - "item": 262, - "order": 10, - "show": 21 - }, - "model": "l8pr.itemsrelationship", - "pk": 262 - }, - { - "fields": { - "item": 263, - "order": 11, - "show": 21 - }, - "model": "l8pr.itemsrelationship", - "pk": 263 - }, - { - "fields": { - "item": 264, - "order": 12, - "show": 21 - }, - "model": "l8pr.itemsrelationship", - "pk": 264 - }, - { - "fields": { - "item": 265, - "order": 13, - "show": 21 - }, - "model": "l8pr.itemsrelationship", - "pk": 265 - }, - { - "fields": { - "item": 266, - "order": 14, - "show": 21 - }, - "model": "l8pr.itemsrelationship", - "pk": 266 - }, - { - "fields": { - "item": 267, - "order": 15, - "show": 21 - }, - "model": "l8pr.itemsrelationship", - "pk": 267 - }, - { - "fields": { - "item": 268, - "order": 16, - "show": 21 - }, - "model": "l8pr.itemsrelationship", - "pk": 268 - }, - { - "fields": { - "item": 269, - "order": 17, - "show": 21 - }, - "model": "l8pr.itemsrelationship", - "pk": 269 - }, - { - "fields": { - "item": 270, - "order": 18, - "show": 21 - }, - "model": "l8pr.itemsrelationship", - "pk": 270 - }, - { - "fields": { - "item": 271, - "order": 19, - "show": 21 - }, - "model": "l8pr.itemsrelationship", - "pk": 271 - }, - { - "fields": { - "item": 272, - "order": 20, - "show": 21 - }, - "model": "l8pr.itemsrelationship", - "pk": 272 - }, - { - "fields": { - "item": 319, - "order": 0, - "show": 25 - }, - "model": "l8pr.itemsrelationship", - "pk": 319 - }, - { - "fields": { - "item": 402, - "order": 0, - "show": 28 - }, - "model": "l8pr.itemsrelationship", - "pk": 402 - }, - { - "fields": { - "item": 403, - "order": 1, - "show": 28 - }, - "model": "l8pr.itemsrelationship", - "pk": 403 - }, - { - "fields": { - "item": 404, - "order": 2, - "show": 28 - }, - "model": "l8pr.itemsrelationship", - "pk": 404 - }, - { - "fields": { - "item": 405, - "order": 3, - "show": 28 - }, - "model": "l8pr.itemsrelationship", - "pk": 405 - }, - { - "fields": { - "item": 406, - "order": 4, - "show": 28 - }, - "model": "l8pr.itemsrelationship", - "pk": 406 - }, - { - "fields": { - "item": 407, - "order": 5, - "show": 28 - }, - "model": "l8pr.itemsrelationship", - "pk": 407 - }, - { - "fields": { - "item": 408, - "order": 6, - "show": 28 - }, - "model": "l8pr.itemsrelationship", - "pk": 408 - }, - { - "fields": { - "item": 409, - "order": 7, - "show": 28 - }, - "model": "l8pr.itemsrelationship", - "pk": 409 - }, - { - "fields": { - "item": 410, - "order": 8, - "show": 28 - }, - "model": "l8pr.itemsrelationship", - "pk": 410 - }, - { - "fields": { - "item": 411, - "order": 9, - "show": 28 - }, - "model": "l8pr.itemsrelationship", - "pk": 411 - }, - { - "fields": { - "item": 412, - "order": 10, - "show": 28 - }, - "model": "l8pr.itemsrelationship", - "pk": 412 - }, - { - "fields": { - "item": 413, - "order": 11, - "show": 28 - }, - "model": "l8pr.itemsrelationship", - "pk": 413 - }, - { - "fields": { - "item": 414, - "order": 12, - "show": 28 - }, - "model": "l8pr.itemsrelationship", - "pk": 414 - }, - { - "fields": { - "item": 415, - "order": 13, - "show": 28 - }, - "model": "l8pr.itemsrelationship", - "pk": 415 - }, - { - "fields": { - "item": 416, - "order": 14, - "show": 28 - }, - "model": "l8pr.itemsrelationship", - "pk": 416 - }, - { - "fields": { - "item": 417, - "order": 15, - "show": 28 - }, - "model": "l8pr.itemsrelationship", - "pk": 417 - }, - { - "fields": { - "item": 418, - "order": 16, - "show": 28 - }, - "model": "l8pr.itemsrelationship", - "pk": 418 - }, - { - "fields": { - "item": 419, - "order": 17, - "show": 28 - }, - "model": "l8pr.itemsrelationship", - "pk": 419 - }, - { - "fields": { - "added": "2016-06-28T21:15:15.131Z", - "author_name": "FilmAt11tv", - "description": null, - "duration": 174, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=TKGFWAT2WME\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/TKGFWAT2WME/hqdefault.jpg", - "title": "THE UNCONDEMNED HD", - "updated": "2016-06-28T21:15:15.131Z", - "url": "https://www.youtube.com/watch?v=TKGFWAT2WME" - }, - "model": "l8pr.item", - "pk": 7 - }, - { - "fields": { - "added": "2016-06-28T21:15:15.192Z", - "author_name": "MetalSucks", - "description": null, - "duration": 124, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=wp_JrDkNrAY\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/wp_JrDkNrAY/hqdefault.jpg", - "title": "Black Metal Chopin \"Prelude in E-Minor\" | MetalSucks", - "updated": "2016-06-28T21:15:15.192Z", - "url": "https://www.youtube.com/watch?v=wp_JrDkNrAY" - }, - "model": "l8pr.item", - "pk": 8 - }, - { - "fields": { - "added": "2016-06-28T21:15:15.215Z", - "author_name": "Sound Flaw", - "description": null, - "duration": 282, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=oS0geQsfcHk\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/oS0geQsfcHk/hqdefault.jpg", - "title": "Kendrick Lamar Performs On Late Show with Stephen Colbert", - "updated": "2016-06-28T21:15:15.215Z", - "url": "https://www.youtube.com/watch?v=oS0geQsfcHk" - }, - "model": "l8pr.item", - "pk": 9 - }, - { - "fields": { - "added": "2016-06-28T21:15:15.242Z", - "author_name": "Claude Klauss", - "description": null, - "duration": 233, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=gMZtDvyxXE4\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/gMZtDvyxXE4/hqdefault.jpg", - "title": "Tom Browne - Funkin' for Jamaica", - "updated": "2016-06-28T21:15:15.242Z", - "url": "https://www.youtube.com/watch?v=gMZtDvyxXE4" - }, - "model": "l8pr.item", - "pk": 10 - }, - { - "fields": { - "added": "2016-06-28T21:15:15.266Z", - "author_name": "looping800", - "description": null, - "duration": 5076, - "html": null, - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/vVDgZaOpYfA/hqdefault.jpg", - "title": "Dire Straits TV-HD - Rockpalast - 1979", - "updated": "2016-06-28T21:15:15.266Z", - "url": "https://www.youtube.com/watch?v=vVDgZaOpYfA" - }, - "model": "l8pr.item", - "pk": 11 - }, - { - "fields": { - "added": "2016-06-28T21:15:15.294Z", - "author_name": "Steve Seagulls", - "description": null, - "duration": 247, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=e4Ao-iNPPUc\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/e4Ao-iNPPUc/hqdefault.jpg", - "title": "Thunderstruck by Steve'n'Seagulls (LIVE)", - "updated": "2016-06-28T21:15:15.294Z", - "url": "https://www.youtube.com/watch?v=e4Ao-iNPPUc" - }, - "model": "l8pr.item", - "pk": 12 - }, - { - "fields": { - "added": "2016-06-28T21:15:16.459Z", - "author_name": "winmic7", - "description": null, - "duration": 130, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=gG62zay3kck\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/gG62zay3kck/hqdefault.jpg", - "title": "Rhabarberbarbara", - "updated": "2016-06-28T21:15:16.459Z", - "url": "https://www.youtube.com/watch?v=gG62zay3kck" - }, - "model": "l8pr.item", - "pk": 32 - }, - { - "fields": { - "added": "2016-06-28T21:15:16.489Z", - "author_name": "Lin Ruben", - "description": null, - "duration": 510, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=LllUpJoFCSc\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/LllUpJoFCSc/hqdefault.jpg", - "title": "\u4fdd\u9f61\u7403 \u8ab0\u80fd\u8207\u4ed6\u6bd4.... \u7403\u795e ? When you get too bored at bowling. Impressive trick shots \u7576\u4f60\u611f\u5230\u592a\u53ad\u7169\u6253\u4fdd\u9f61\u7403\u3002\u4ee4\u4eba\u5370\u8c61\u6df1\u523b\u7684\u4f0e\u80fd\u62cd\u651d", - "updated": "2016-06-28T21:15:16.489Z", - "url": "https://www.youtube.com/watch?v=LllUpJoFCSc" - }, - "model": "l8pr.item", - "pk": 33 - }, - { - "fields": { - "added": "2016-06-28T21:15:16.516Z", - "author_name": "pigwiththefaceofaboy", - "description": null, - "duration": 408, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=hWTFG3J1CP8\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/hWTFG3J1CP8/hqdefault.jpg", - "title": "Complete History Of The Soviet Union, Arranged To The Melody Of Tetris", - "updated": "2016-06-28T21:15:16.517Z", - "url": "https://www.youtube.com/watch?v=hWTFG3J1CP8" - }, - "model": "l8pr.item", - "pk": 34 - }, - { - "fields": { - "added": "2016-06-28T21:15:16.543Z", - "author_name": "\u041f\u0430\u0448\u0430 \u0411\u0443\u043c\u0447\u0438\u043a", - "description": null, - "duration": 272, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=w3c67AYl0rM\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/w3c67AYl0rM/hqdefault.jpg", - "title": "A ride on the roof of the metro train | \u041f\u043e\u0435\u0437\u0434\u043a\u0430 \u043d\u0430 \u043a\u0440\u044b\u0448\u0435 \u043c\u0435\u0442\u0440\u043e", - "updated": "2016-06-28T21:15:16.544Z", - "url": "https://www.youtube.com/watch?v=w3c67AYl0rM" - }, - "model": "l8pr.item", - "pk": 35 - }, - { - "fields": { - "added": "2016-06-28T21:15:16.570Z", - "author_name": "CHESTBRAS", - "description": null, - "duration": 59, - "html": null, - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/InMJopurNTE/hqdefault.jpg", - "title": "CHEST-BRAS", - "updated": "2016-06-28T21:15:16.570Z", - "url": "https://www.youtube.com/watch?v=InMJopurNTE" - }, - "model": "l8pr.item", - "pk": 36 - }, - { - "fields": { - "added": "2016-06-28T21:15:16.596Z", - "author_name": "Animal Planet", - "description": null, - "duration": 153, - "html": null, - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/mVusrd1jXqc/hqdefault.jpg", - "title": "High-Tech Camel Races | Wild Arabia", - "updated": "2016-06-28T21:15:16.596Z", - "url": "https://www.youtube.com/watch?v=mVusrd1jXqc" - }, - "model": "l8pr.item", - "pk": 37 - }, - { - "fields": { - "added": "2016-06-28T21:15:16.667Z", - "author_name": "Soulwax Rarities", - "description": null, - "duration": 371, - "html": "\n
\n
\n \n
\n \n
\n\n \n \n \n \n \n \n
SoundCloud\n https://soundcloud.com/soulwax-rarities/pulp-after\n
\n
\n
\n", - "provider_name": "SoundCloud", - "thumbnail": "http://i1.sndcdn.com/artworks-000045559533-txyvdr-t500x500.jpg", - "title": "Pulp - After You (Soulwax Remix) HQ by Soulwax Rarities", - "updated": "2016-06-28T21:15:16.667Z", - "url": "https://soundcloud.com/soulwax-rarities/pulp-after-you-soulwax-remix-hq?in=benetco/sets/slwx" - }, - "model": "l8pr.item", - "pk": 38 - }, - { - "fields": { - "added": "2016-06-28T21:15:16.692Z", - "author_name": "GOOSE", - "description": null, - "duration": 222, - "html": "\n
\n
\n \n
\n \n
\n\n \n \n \n \n \n \n
SoundCloud\n https://soundcloud.com/goosemusic/synrise-soulwax-\n
\n
\n
\n", - "provider_name": "SoundCloud", - "thumbnail": "http://i1.sndcdn.com/artworks-000050310237-wlru8o-t500x500.jpg", - "title": "SYNRISE - SOULWAX REMIX by GOOSE", - "updated": "2016-06-28T21:15:16.692Z", - "url": "https://soundcloud.com/goosemusic/synrise-soulwax-remix?in=benetco/sets/slwx" - }, - "model": "l8pr.item", - "pk": 39 - }, - { - "fields": { - "added": "2016-06-28T21:15:16.718Z", - "author_name": "Soulwax Rarities", - "description": null, - "duration": 371, - "html": "\n
\n
\n \n
\n \n
\n\n \n \n \n \n \n \n
SoundCloud\n https://soundcloud.com/soulwax-rarities/pulp-after\n
\n
\n
\n", - "provider_name": "SoundCloud", - "thumbnail": "http://i1.sndcdn.com/artworks-000045559533-txyvdr-t500x500.jpg", - "title": "Pulp - After You (Soulwax Remix) HQ by Soulwax Rarities", - "updated": "2016-06-28T21:15:16.718Z", - "url": "https://soundcloud.com/soulwax-rarities/pulp-after-you-soulwax-remix-hq?in=benetco/sets/slwx" - }, - "model": "l8pr.item", - "pk": 40 - }, - { - "fields": { - "added": "2016-06-28T21:15:16.745Z", - "author_name": "soulwax", - "description": null, - "duration": 325, - "html": "\n
\n
\n \n
\n \n
\n\n \n \n \n \n \n \n
SoundCloud\n https://soundcloud.com/soulwaxofficial/krack-nite-\n
\n
\n
\n", - "provider_name": "SoundCloud", - "thumbnail": "http://i1.sndcdn.com/artworks-000004366685-x5u8lv-t500x500.jpg", - "title": "Krack (Nite Version) by soulwax", - "updated": "2016-06-28T21:15:16.745Z", - "url": "https://soundcloud.com/soulwaxofficial/krack-nite-version?in=benetco/sets/slwx" - }, - "model": "l8pr.item", - "pk": 41 - }, - { - "fields": { - "added": "2016-06-28T21:15:16.775Z", - "author_name": "Phantasy Sound", - "description": null, - "duration": 157, - "html": "\n
\n
\n \n
\n \n
\n\n \n \n \n \n \n \n
SoundCloud\n https://soundcloud.com/phantasysound/best-in-class\n
\n
\n
\n", - "provider_name": "SoundCloud", - "thumbnail": "http://i1.sndcdn.com/artworks-000002133352-hjxzm3-t500x500.jpg", - "title": "Late Of The Pier - Best In Class (Soulwax Remix) by Phantasy Sound", - "updated": "2016-06-28T21:15:16.775Z", - "url": "https://soundcloud.com/phantasysound/best-in-class-soulwax-remix?in=benetco/sets/slwx" - }, - "model": "l8pr.item", - "pk": 42 - }, - { - "fields": { - "added": "2016-06-28T21:15:16.802Z", - "author_name": "Soulwax Rarities", - "description": null, - "duration": 327, - "html": "\n
\n
\n \n
\n \n
\n\n \n \n \n \n \n \n
SoundCloud\n https://soundcloud.com/soulwax-rarities/kolk-uma-s\n
\n
\n
\n", - "provider_name": "SoundCloud", - "thumbnail": "http://i1.sndcdn.com/artworks-000055967012-5vegw4-t500x500.jpg", - "title": "Kolk - Uma (Soulwax Remix) HQ CD-Rip by Soulwax Rarities", - "updated": "2016-06-28T21:15:16.802Z", - "url": "https://soundcloud.com/soulwax-rarities/kolk-uma-soulwax-remix-hq-cd?in=benetco/sets/slwx" - }, - "model": "l8pr.item", - "pk": 43 - }, - { - "fields": { - "added": "2016-06-28T21:15:16.837Z", - "author_name": "princesskhym", - "description": null, - "duration": 343, - "html": "\n
\n
\n \n
\n \n
\n\n \n \n \n \n \n \n
SoundCloud\n https://soundcloud.com/princesskhym/gorillaz-dare-\n
\n
\n
\n", - "provider_name": "SoundCloud", - "thumbnail": "http://a1.sndcdn.com/images/fb_placeholder.png?1440411592", - "title": "Gorillaz DARE (Soulwax Remix) by princesskhym", - "updated": "2016-06-28T21:15:16.837Z", - "url": "https://soundcloud.com/princesskhym/gorillaz-dare-soulwax-remix?in=benetco/sets/slwx" - }, - "model": "l8pr.item", - "pk": 44 - }, - { - "fields": { - "added": "2016-06-28T21:15:16.869Z", - "author_name": "Banger L.", - "description": null, - "duration": 445, - "html": "\n
\n
\n \n
\n \n
\n\n \n \n \n \n \n \n
SoundCloud\n https://soundcloud.com/banger-live/justice-phantom\n
\n
\n
\n", - "provider_name": "SoundCloud", - "thumbnail": "http://a1.sndcdn.com/images/fb_placeholder.png?1440411592", - "title": "Justice - Phantom Pt 2 Soulwax Remix by Banger L.", - "updated": "2016-06-28T21:15:16.869Z", - "url": "https://soundcloud.com/banger-live/justice-phantom-pt-2-soulwax?in=benetco/sets/slwx" - }, - "model": "l8pr.item", - "pk": 45 - }, - { - "fields": { - "added": "2016-06-28T21:15:16.898Z", - "author_name": "JLMoloney", - "description": null, - "duration": 2567, - "html": "\n
\n
\n \n
\n \n
\n\n \n \n \n \n \n \n
SoundCloud\n https://soundcloud.com/coversjm/soulwax-fm-gta-v-c\n
\n
\n
\n", - "provider_name": "SoundCloud", - "thumbnail": "http://i1.sndcdn.com/artworks-000062449428-j60yzw-t500x500.jpg", - "title": "Soulwax FM GTA V Continuous Mix by JLMoloney", - "updated": "2016-06-28T21:15:16.898Z", - "url": "https://soundcloud.com/coversjm/soulwax-fm-gta-v-continouis?in=benetco/sets/slwx" - }, - "model": "l8pr.item", - "pk": 46 - }, - { - "fields": { - "added": "2016-06-28T21:15:19.638Z", - "author_name": "lesamisdelaforet", - "description": null, - "duration": 230, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=2mBcoTRQPqI\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/2mBcoTRQPqI/hqdefault.jpg", - "title": "CCCP - cosmic communist constructions photographed - frederic chaubin", - "updated": "2016-06-28T21:15:19.638Z", - "url": "https://www.youtube.com/watch?v=2mBcoTRQPqI" - }, - "model": "l8pr.item", - "pk": 91 - }, - { - "fields": { - "added": "2016-06-28T21:15:19.698Z", - "author_name": "BGbitch", - "description": null, - "duration": 1522, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=OGJl7cEMhys\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/OGJl7cEMhys/hqdefault.jpg", - "title": "BIRDGANG full", - "updated": "2016-06-28T21:15:19.698Z", - "url": "https://www.youtube.com/watch?v=OGJl7cEMhys" - }, - "model": "l8pr.item", - "pk": 92 - }, - { - "fields": { - "added": "2016-06-28T21:15:19.750Z", - "author_name": "CuriousInventor", - "description": null, - "duration": 1345, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=Lx9zgZCMqXE\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/Lx9zgZCMqXE/hqdefault.jpg", - "title": "How Bitcoin Works Under the Hood", - "updated": "2016-06-28T21:15:19.750Z", - "url": "https://www.youtube.com/watch?v=Lx9zgZCMqXE" - }, - "model": "l8pr.item", - "pk": 93 - }, - { - "fields": { - "added": "2016-06-28T21:15:19.791Z", - "author_name": "kaptainkristian", - "description": null, - "duration": 382, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=-xzzQVk5IfE\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/-xzzQVk5IfE/hqdefault.jpg", - "title": "Calvin & Hobbes - Art Before Commerce", - "updated": "2016-06-28T21:15:19.791Z", - "url": "https://www.youtube.com/watch?v=-xzzQVk5IfE" - }, - "model": "l8pr.item", - "pk": 94 - }, - { - "fields": { - "added": "2016-06-28T21:15:19.853Z", - "author_name": "Hershel Layton", - "description": null, - "duration": 537, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=vKM154z0Afs\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/vKM154z0Afs/hqdefault.jpg", - "title": "Jean Cocteau Documentary #3", - "updated": "2016-06-28T21:15:19.853Z", - "url": "https://www.youtube.com/watch?v=vKM154z0Afs" - }, - "model": "l8pr.item", - "pk": 95 - }, - { - "fields": { - "added": "2016-06-28T21:15:19.890Z", - "author_name": "MOCA", - "description": null, - "duration": 631, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=2B6nMlajUqU\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/2B6nMlajUqU/hqdefault.jpg", - "title": "Nan Goldin - The Ballad of Sexual Dependency - MOCA U - MOCAtv", - "updated": "2016-06-28T21:15:19.890Z", - "url": "https://www.youtube.com/watch?v=2B6nMlajUqU" - }, - "model": "l8pr.item", - "pk": 96 - }, - { - "fields": { - "added": "2016-06-28T21:15:19.954Z", - "author_name": "VD Vault", - "description": null, - "duration": 3586, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=c4sLwt8mhZs\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/c4sLwt8mhZs/hqdefault.jpg", - "title": "KATE BUSH - The Kate Bush Story (2014 BBC Documentary)", - "updated": "2016-06-28T21:15:19.954Z", - "url": "https://www.youtube.com/watch?v=c4sLwt8mhZs" - }, - "model": "l8pr.item", - "pk": 97 - }, - { - "fields": { - "added": "2016-06-28T21:15:20.008Z", - "author_name": "Jungles in Paris", - "description": null, - "duration": 347, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=eUSBVuBXnaY\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/eUSBVuBXnaY/hqdefault.jpg", - "title": "The Dictionary", - "updated": "2016-06-28T21:15:20.008Z", - "url": "https://www.youtube.com/watch?v=eUSBVuBXnaY" - }, - "model": "l8pr.item", - "pk": 98 - }, - { - "fields": { - "added": "2016-06-28T21:15:20.048Z", - "author_name": "Motherboard", - "description": null, - "duration": 1851, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=Fx93WJPCCGs\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/Fx93WJPCCGs/hqdefault.jpg", - "title": "Free the Network: Hackers Take Back the Web", - "updated": "2016-06-28T21:15:20.048Z", - "url": "https://www.youtube.com/watch?v=Fx93WJPCCGs" - }, - "model": "l8pr.item", - "pk": 99 - }, - { - "fields": { - "added": "2016-06-28T21:15:20.090Z", - "author_name": "The Economist", - "description": null, - "duration": 380, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=AOhuUxtxrmg\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/AOhuUxtxrmg/hqdefault.jpg", - "title": "Japan's Yakuza: Inside the syndicate", - "updated": "2016-06-28T21:15:20.090Z", - "url": "https://www.youtube.com/watch?v=AOhuUxtxrmg" - }, - "model": "l8pr.item", - "pk": 100 - }, - { - "fields": { - "added": "2016-06-28T21:15:20.141Z", - "author_name": "lamiamarca", - "description": null, - "duration": 144, - "html": null, - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/ZJIhMz8Auts/hqdefault.jpg", - "title": "A simetria dos filmes de Wes Anderson", - "updated": "2016-06-28T21:15:20.142Z", - "url": "https://www.youtube.com/watch?v=ZJIhMz8Auts" - }, - "model": "l8pr.item", - "pk": 101 - }, - { - "fields": { - "added": "2016-06-28T21:15:20.200Z", - "author_name": "canzoniutili", - "description": null, - "duration": 225, - "html": null, - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/O6dF8Gjm-X8/hqdefault.jpg", - "title": "Marina Abramovic - ARTIST IS PRESENT (La muraglia cinese)", - "updated": "2016-06-28T21:15:20.200Z", - "url": "https://www.youtube.com/watch?v=O6dF8Gjm-X8" - }, - "model": "l8pr.item", - "pk": 102 - }, - { - "fields": { - "added": "2016-06-28T21:15:20.234Z", - "author_name": "The Economist", - "description": null, - "duration": 630, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=OKKbkS_LOu4\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/OKKbkS_LOu4/hqdefault.jpg", - "title": "Why does time pass?", - "updated": "2016-06-28T21:15:20.235Z", - "url": "https://www.youtube.com/watch?v=OKKbkS_LOu4" - }, - "model": "l8pr.item", - "pk": 103 - }, - { - "fields": { - "added": "2016-06-28T21:15:20.276Z", - "author_name": "The Economist", - "description": null, - "duration": 538, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=Rx7erWZ8TjA\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/Rx7erWZ8TjA/hqdefault.jpg", - "title": "Do we live in a multiverse?", - "updated": "2016-06-28T21:15:20.276Z", - "url": "https://www.youtube.com/watch?v=Rx7erWZ8TjA" - }, - "model": "l8pr.item", - "pk": 104 - }, - { - "fields": { - "added": "2016-06-28T21:15:20.316Z", - "author_name": "azwris", - "description": null, - "duration": 330, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=VOhQ1hBUgkw\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/VOhQ1hBUgkw/hqdefault.jpg", - "title": "\"Jeu d'\u00e9checs\" avec Marcel Duchamp ,1963 / excerpts", - "updated": "2016-06-28T21:15:20.317Z", - "url": "https://www.youtube.com/watch?v=VOhQ1hBUgkw" - }, - "model": "l8pr.item", - "pk": 105 - }, - { - "fields": { - "added": "2016-06-28T21:15:20.392Z", - "author_name": "KOMPAKT", - "description": null, - "duration": 417, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=NuzmJu0n2lQ\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/NuzmJu0n2lQ/hqdefault.jpg", - "title": "Gregor Schwellenbach spielt 20 Jahre Kompakt (The Making Of)", - "updated": "2016-06-28T21:15:20.392Z", - "url": "https://www.youtube.com/watch?v=NuzmJu0n2lQ" - }, - "model": "l8pr.item", - "pk": 106 - }, - { - "fields": { - "added": "2016-06-28T21:15:20.473Z", - "author_name": "Parcours de la pens\u00e9e", - "description": null, - "duration": 294, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=B21ZD8Q_uJE\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/B21ZD8Q_uJE/hqdefault.jpg", - "title": "Op\u00e9ra vertical (fin)", - "updated": "2016-06-28T21:15:20.473Z", - "url": "https://www.youtube.com/watch?v=B21ZD8Q_uJE" - }, - "model": "l8pr.item", - "pk": 107 - }, - { - "fields": { - "added": "2016-06-28T21:15:20.520Z", - "author_name": "Gainsbarr\u00e924", - "description": null, - "duration": 1632, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=GV-b831aoL8\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/GV-b831aoL8/hqdefault.jpg", - "title": "Gainsbourg.Nous nous sommes tant aim\u00e9s.avi", - "updated": "2016-06-28T21:15:20.520Z", - "url": "https://www.youtube.com/watch?v=GV-b831aoL8" - }, - "model": "l8pr.item", - "pk": 108 - }, - { - "fields": { - "added": "2016-06-28T21:15:20.570Z", - "author_name": "kourtrajmeofficial", - "description": null, - "duration": 324, - "html": null, - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/fpfkjX_hmsc/hqdefault.jpg", - "title": "Hommage au film LA HAINE", - "updated": "2016-06-28T21:15:20.570Z", - "url": "https://www.youtube.com/watch?v=fpfkjX_hmsc" - }, - "model": "l8pr.item", - "pk": 109 - }, - { - "fields": { - "added": "2016-06-28T21:15:20.641Z", - "author_name": "NASA Goddard", - "description": null, - "duration": 160, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=rA_VCLzvbvM\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/rA_VCLzvbvM/hqdefault.jpg", - "title": "NASA | Getting the Big Picture", - "updated": "2016-06-28T21:15:20.641Z", - "url": "https://www.youtube.com/watch?v=rA_VCLzvbvM" - }, - "model": "l8pr.item", - "pk": 110 - }, - { - "fields": { - "added": "2016-06-28T21:15:20.711Z", - "author_name": "Aitor L\u00f3pez G\u00f3mez", - "description": null, - "duration": 166, - "html": null, - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/DY5yrEAvCQc/hqdefault.jpg", - "title": "Breaking Bad POV (Point of View)", - "updated": "2016-06-28T21:15:20.711Z", - "url": "https://www.youtube.com/watch?v=DY5yrEAvCQc" - }, - "model": "l8pr.item", - "pk": 111 - }, - { - "fields": { - "added": "2016-06-28T21:15:20.747Z", - "author_name": "Motherboard", - "description": null, - "duration": 1720, - "html": null, - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/5qBjFZV19p0/hqdefault.jpg", - "title": "The Dawn of Killer Robots (Full Length)", - "updated": "2016-06-28T21:15:20.747Z", - "url": "https://www.youtube.com/watch?v=5qBjFZV19p0" - }, - "model": "l8pr.item", - "pk": 112 - }, - { - "fields": { - "added": "2016-06-28T21:15:20.824Z", - "author_name": "Federico Fiorani", - "description": null, - "duration": 1754, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=5ZIxuj7cbeg\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/5ZIxuj7cbeg/hqdefault.jpg", - "title": "Free Climbing Patrick Edlinger Verdon Forever", - "updated": "2016-06-28T21:15:20.824Z", - "url": "https://www.youtube.com/watch?v=5ZIxuj7cbeg" - }, - "model": "l8pr.item", - "pk": 113 - }, - { - "fields": { - "added": "2016-06-28T21:15:20.890Z", - "author_name": "Motherboard", - "description": null, - "duration": 715, - "html": null, - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/4IlafxWwKdY/hqdefault.jpg", - "title": "Instagram's Most Notorious Dissector of Death", - "updated": "2016-06-28T21:15:20.891Z", - "url": "https://www.youtube.com/watch?v=4IlafxWwKdY" - }, - "model": "l8pr.item", - "pk": 114 - }, - { - "fields": { - "added": "2016-06-28T21:15:20.972Z", - "author_name": "MAMMUT", - "description": null, - "duration": 387, - "html": null, - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/dMGZTIMRIIo/hqdefault.jpg", - "title": "KINETIC - Jan Hojer in Fontainebleau", - "updated": "2016-06-28T21:15:20.972Z", - "url": "https://www.youtube.com/watch?v=dMGZTIMRIIo" - }, - "model": "l8pr.item", - "pk": 115 - }, - { - "fields": { - "added": "2016-06-28T21:15:21.054Z", - "author_name": "ProjectLikeKnowsLike", - "description": null, - "duration": 333, - "html": null, - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/5pGg-te_kCI/hqdefault.jpg", - "title": "Brandon Doman - Like Knows Like", - "updated": "2016-06-28T21:15:21.054Z", - "url": "https://www.youtube.com/watch?v=5pGg-te_kCI" - }, - "model": "l8pr.item", - "pk": 116 - }, - { - "fields": { - "added": "2016-06-28T21:15:21.122Z", - "author_name": "ProjectLikeKnowsLike", - "description": null, - "duration": 448, - "html": null, - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/YG_Ipmux2cM/hqdefault.jpg", - "title": "Jessica Walsh - Like Knows Like", - "updated": "2016-06-28T21:15:21.122Z", - "url": "https://www.youtube.com/watch?v=YG_Ipmux2cM" - }, - "model": "l8pr.item", - "pk": 117 - }, - { - "fields": { - "added": "2016-06-28T21:15:21.184Z", - "author_name": "JokeMTP", - "description": null, - "duration": 398, - "html": null, - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/kugIhNFE60I/hqdefault.jpg", - "title": "Joke: \"De Kyoto \u00e0...\" - \u00c9pisode 1", - "updated": "2016-06-28T21:15:21.184Z", - "url": "https://www.youtube.com/watch?v=kugIhNFE60I" - }, - "model": "l8pr.item", - "pk": 118 - }, - { - "fields": { - "added": "2016-06-28T21:15:21.279Z", - "author_name": "Fondation Cartier pour l'art contemporain", - "description": null, - "duration": 284, - "html": null, - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/g2iPpa4azRc/hqdefault.jpg", - "title": "Ron Mueck - Still Life: Ron Mueck at Work - 2013", - "updated": "2016-06-28T21:15:21.279Z", - "url": "https://www.youtube.com/watch?v=g2iPpa4azRc" - }, - "model": "l8pr.item", - "pk": 119 - }, - { - "fields": { - "added": "2016-06-28T21:15:21.364Z", - "author_name": "John Lyons", - "description": null, - "duration": 510, - "html": null, - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/BifeKHFWTZ8/hqdefault.jpg", - "title": "Linklater: On Cinema & Time", - "updated": "2016-06-28T21:15:21.364Z", - "url": "https://www.youtube.com/watch?v=BifeKHFWTZ8" - }, - "model": "l8pr.item", - "pk": 120 - }, - { - "fields": { - "added": "2016-06-28T21:15:21.427Z", - "author_name": "Ci Neryong", - "description": null, - "duration": 296, - "html": null, - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/odJxAd4WU8Y/hqdefault.jpg", - "title": "What is neorealism [HD]", - "updated": "2016-06-28T21:15:21.427Z", - "url": "https://www.youtube.com/watch?v=odJxAd4WU8Y" - }, - "model": "l8pr.item", - "pk": 121 - }, - { - "fields": { - "added": "2016-06-28T21:15:21.511Z", - "author_name": "NOWNESS", - "description": null, - "duration": 192, - "html": null, - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/A0EVqd3wbq0/hqdefault.jpg", - "title": "\u201cMad River\u201d by Jungles in Paris", - "updated": "2016-06-28T21:15:21.511Z", - "url": "https://www.youtube.com/watch?v=A0EVqd3wbq0" - }, - "model": "l8pr.item", - "pk": 122 - }, - { - "fields": { - "added": "2016-06-28T21:15:27.329Z", - "author_name": "Shower Covers", - "description": null, - "duration": 58, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=xEZx5S_M8oY\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/xEZx5S_M8oY/hqdefault.jpg", - "title": "How to keep a leg cast, wound, dressing or drain dry with the SHOWER BOOT", - "updated": "2016-06-28T21:15:27.329Z", - "url": "https://www.youtube.com/watch?v=xEZx5S_M8oY" - }, - "model": "l8pr.item", - "pk": 252 - }, - { - "fields": { - "added": "2016-06-28T21:15:27.358Z", - "author_name": "Devine Lu Linvega", - "description": null, - "duration": 296, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=sUvKDi-cFng\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/sUvKDi-cFng/hqdefault.jpg", - "title": "Purgateus - Proteus Mod", - "updated": "2016-06-28T21:15:27.358Z", - "url": "https://www.youtube.com/watch?v=sUvKDi-cFng" - }, - "model": "l8pr.item", - "pk": 253 - }, - { - "fields": { - "added": "2016-06-28T21:15:27.383Z", - "author_name": "Benjamin Etco", - "description": null, - "duration": 19, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=ZUADfG-wqbA\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/ZUADfG-wqbA/hqdefault.jpg", - "title": "Indoor Homemade Skatepark", - "updated": "2016-06-28T21:15:27.383Z", - "url": "https://www.youtube.com/watch?v=ZUADfG-wqbA" - }, - "model": "l8pr.item", - "pk": 254 - }, - { - "fields": { - "added": "2016-06-28T21:15:27.408Z", - "author_name": "Benjamin Etco", - "description": null, - "duration": 121, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://youtu.be/6pHIaM682zY\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/6pHIaM682zY/hqdefault.jpg", - "title": "SlideShow2", - "updated": "2016-06-28T21:15:27.409Z", - "url": "https://youtu.be/6pHIaM682zY" - }, - "model": "l8pr.item", - "pk": 255 - }, - { - "fields": { - "added": "2016-06-28T21:15:27.441Z", - "author_name": "bepsdwardo", - "description": null, - "duration": 276, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://youtu.be/Bp9brL8NLHA?t=12s\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/Bp9brL8NLHA/hqdefault.jpg", - "title": "Du Hoang Ho au Yang Tse Kiang (skip to s)", - "updated": "2016-06-28T21:15:27.441Z", - "url": "https://youtu.be/Bp9brL8NLHA?t=12s" - }, - "model": "l8pr.item", - "pk": 256 - }, - { - "fields": { - "added": "2016-06-28T21:15:27.470Z", - "author_name": "Stephane Rituit", - "description": null, - "duration": 100, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=xEC3WPzhGko\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/xEC3WPzhGko/hqdefault.jpg", - "title": "Un singe en hiver: l'ivresse", - "updated": "2016-06-28T21:15:27.470Z", - "url": "https://www.youtube.com/watch?v=xEC3WPzhGko" - }, - "model": "l8pr.item", - "pk": 257 - }, - { - "fields": { - "added": "2016-06-28T21:15:27.498Z", - "author_name": "Safa Safaup", - "description": null, - "duration": 230, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=5IJte841c4A\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/5IJte841c4A/hqdefault.jpg", - "title": "The Story Of Bow & Arrow Marina Abramovic\u0301\ufeff", - "updated": "2016-06-28T21:15:27.498Z", - "url": "https://www.youtube.com/watch?v=5IJte841c4A" - }, - "model": "l8pr.item", - "pk": 258 - }, - { - "fields": { - "added": "2016-06-28T21:15:27.525Z", - "author_name": "avigdor samuel jimenez rendon", - "description": null, - "duration": 335, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=UqOOZux5sPE\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/UqOOZux5sPE/hqdefault.jpg", - "title": "2001: A Space Odyssey-Strauss", - "updated": "2016-06-28T21:15:27.525Z", - "url": "https://www.youtube.com/watch?v=UqOOZux5sPE" - }, - "model": "l8pr.item", - "pk": 259 - }, - { - "fields": { - "added": "2016-06-28T21:15:27.548Z", - "author_name": "NOWNESS", - "description": null, - "duration": 258, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=7ooW3hp7og4\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/7ooW3hp7og4/hqdefault.jpg", - "title": "#DefineBeauty Pt 3: Sexual impulse under the microscope in \"Cr\u00e8me Caramel\"", - "updated": "2016-06-28T21:15:27.548Z", - "url": "https://www.youtube.com/watch?v=7ooW3hp7og4" - }, - "model": "l8pr.item", - "pk": 260 - }, - { - "fields": { - "added": "2016-06-28T21:15:27.573Z", - "author_name": "bitlength", - "description": null, - "duration": 346, - "html": null, - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/7vvM-MBiddY/hqdefault.jpg", - "title": "Daft Punk's Electroma opening scene.", - "updated": "2016-06-28T21:15:27.573Z", - "url": "https://www.youtube.com/watch?v=7vvM-MBiddY" - }, - "model": "l8pr.item", - "pk": 261 - }, - { - "fields": { - "added": "2016-06-28T21:15:27.605Z", - "author_name": "NeroRush23", - "description": null, - "duration": 776, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=M5QD-32MCv4\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/M5QD-32MCv4/hqdefault.jpg", - "title": "Albert Camus - Discours de r\u00e9ception du prix Nobel, 1957", - "updated": "2016-06-28T21:15:27.605Z", - "url": "https://www.youtube.com/watch?v=M5QD-32MCv4" - }, - "model": "l8pr.item", - "pk": 262 - }, - { - "fields": { - "added": "2016-06-28T21:15:27.635Z", - "author_name": "simonethedog", - "description": null, - "duration": 853, - "html": null, - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/hWY0PYjkX-A/hqdefault.jpg", - "title": "1972-EXPANSION by Toshio Matsumoto.avi", - "updated": "2016-06-28T21:15:27.635Z", - "url": "https://www.youtube.com/watch?v=hWY0PYjkX-A" - }, - "model": "l8pr.item", - "pk": 263 - }, - { - "fields": { - "added": "2016-06-28T21:15:27.669Z", - "author_name": "Usine C Montr\u00e9al", - "description": null, - "duration": 170, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=qAYAuJ9dIlE\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/qAYAuJ9dIlE/hqdefault.jpg", - "title": "HAKANAI de Adrien M / Claire B | TEMPS D'IMAGES - 04, 05 avril 2014 \u00e0 20h30, Usine C", - "updated": "2016-06-28T21:15:27.669Z", - "url": "https://www.youtube.com/watch?v=qAYAuJ9dIlE" - }, - "model": "l8pr.item", - "pk": 264 - }, - { - "fields": { - "added": "2016-06-28T21:15:27.691Z", - "author_name": "JOnas3MokaQ", - "description": null, - "duration": 392, - "html": null, - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/1GFkN4deuZU/hqdefault.jpg", - "title": "Walt Disney's & Salvador Dali - Destino 2003 (HD 1080p)", - "updated": "2016-06-28T21:15:27.691Z", - "url": "https://youtu.be/1GFkN4deuZU" - }, - "model": "l8pr.item", - "pk": 265 - }, - { - "fields": { - "added": "2016-06-28T21:15:27.716Z", - "author_name": "CineFix", - "description": null, - "duration": 201, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=jhkQOj_kkMU\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/jhkQOj_kkMU/hqdefault.jpg", - "title": "Princess Mononoke - 8 Bit Cinema", - "updated": "2016-06-28T21:15:27.716Z", - "url": "https://www.youtube.com/watch?v=jhkQOj_kkMU" - }, - "model": "l8pr.item", - "pk": 266 - }, - { - "fields": { - "added": "2016-06-28T21:15:27.747Z", - "author_name": "fireurgunz", - "description": null, - "duration": 524, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=MYEmL0d0lZE\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/MYEmL0d0lZE/hqdefault.jpg", - "title": "Walt Disney's The Old Mill (1937)", - "updated": "2016-06-28T21:15:27.747Z", - "url": "https://www.youtube.com/watch?v=MYEmL0d0lZE" - }, - "model": "l8pr.item", - "pk": 267 - }, - { - "fields": { - "added": "2016-06-28T21:15:27.771Z", - "author_name": "The CGBros", - "description": null, - "duration": 5, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=Rk0snQAY5Ws\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/Rk0snQAY5Ws/hqdefault.jpg", - "title": "CGI Tech Demo HD: \"Unamed Sound Sculpture\" by - Onformative", - "updated": "2016-06-28T21:15:27.771Z", - "url": "https://www.youtube.com/watch?v=Rk0snQAY5Ws" - }, - "model": "l8pr.item", - "pk": 268 - }, - { - "fields": { - "added": "2016-06-28T21:15:27.798Z", - "author_name": "J. DTRTR", - "description": null, - "duration": 14, - "html": null, - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/BFNsryD1dYA/hqdefault.jpg", - "title": "PASTESTRUDER V0.1 first print", - "updated": "2016-06-28T21:15:27.798Z", - "url": "https://www.youtube.com/watch?v=BFNsryD1dYA" - }, - "model": "l8pr.item", - "pk": 269 - }, - { - "fields": { - "added": "2016-06-28T21:15:27.829Z", - "author_name": "jamie Harley", - "description": null, - "duration": 315, - "html": null, - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/lCZMmKV34Z0/hqdefault.jpg", - "title": "Koudlam : 'Alcoholic's Hymn'", - "updated": "2016-06-28T21:15:27.829Z", - "url": "https://www.youtube.com/watch?v=lCZMmKV34Z0" - }, - "model": "l8pr.item", - "pk": 270 - }, - { - "fields": { - "added": "2016-06-28T21:15:27.867Z", - "author_name": "Duncan Malashock", - "description": null, - "duration": 133, - "html": null, - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/McPVjc6xVT8/hqdefault.jpg", - "title": "Magic Square", - "updated": "2016-06-28T21:15:27.867Z", - "url": "https://youtu.be/McPVjc6xVT8" - }, - "model": "l8pr.item", - "pk": 271 - }, - { - "fields": { - "added": "2016-06-28T21:15:27.898Z", - "author_name": "Duncan Malashock", - "description": null, - "duration": 82, - "html": null, - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/XpTCfnL2bsg/hqdefault.jpg", - "title": "Temple", - "updated": "2016-06-28T21:15:27.898Z", - "url": "https://youtu.be/XpTCfnL2bsg" - }, - "model": "l8pr.item", - "pk": 272 - }, - { - "fields": { - "added": "2016-06-28T21:15:30.458Z", - "author_name": "Parker Lewis 1990", - "description": null, - "duration": 1462, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=fVwGaELhXJE\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/fVwGaELhXJE/hqdefault.jpg", - "title": "Parker Lewis full series season 1 episode 10 - Parker Lewis 1990 new HD", - "updated": "2016-06-28T21:15:30.458Z", - "url": "https://www.youtube.com/watch?v=fVwGaELhXJE" - }, - "model": "l8pr.item", - "pk": 319 - }, - { - "fields": { - "added": "2016-06-28T21:15:34.014Z", - "author_name": "myfairjoker", - "description": null, - "duration": 189, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=MukeH1hWrxY\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/MukeH1hWrxY/hqdefault.jpg", - "title": "Serge Gainsbourg - Ecce homo (Live) - 1981", - "updated": "2016-06-28T21:15:34.014Z", - "url": "https://www.youtube.com/watch?v=MukeH1hWrxY" - }, - "model": "l8pr.item", - "pk": 402 - }, - { - "fields": { - "added": "2016-06-28T21:15:34.111Z", - "author_name": "celipat56", - "description": null, - "duration": 681, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=A23FoIaZf7g\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/A23FoIaZf7g/hqdefault.jpg", - "title": "Serge Gainsbourg et etienne Daho", - "updated": "2016-06-28T21:15:34.111Z", - "url": "https://www.youtube.com/watch?v=A23FoIaZf7g" - }, - "model": "l8pr.item", - "pk": 403 - }, - { - "fields": { - "added": "2016-06-28T21:15:34.209Z", - "author_name": "KOOL SHY Productions", - "description": null, - "duration": 201, - "html": "\n
\n
\n \n
\n \n
\n\n \n \n \n \n \n \n
SoundCloud\n https://soundcloud.com/kool-shy-productions/ennui-\n
\n
\n
\n", - "provider_name": "SoundCloud", - "thumbnail": "http://i1.sndcdn.com/artworks-000135009144-2avnbx-t500x500.jpg", - "title": "Ennui Mineur by KOOL SHY Productions", - "updated": "2016-06-28T21:15:34.209Z", - "url": "https://soundcloud.com/kool-shy-productions/ennui-mineur" - }, - "model": "l8pr.item", - "pk": 404 - }, - { - "fields": { - "added": "2016-06-28T21:15:34.276Z", - "author_name": "FRED lagonbleu", - "description": null, - "duration": 139, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=zjpDtcHS7Sg\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/zjpDtcHS7Sg/hqdefault.jpg", - "title": "Serge Gainsbourg - L'anamour \u266b", - "updated": "2016-06-28T21:15:34.276Z", - "url": "https://www.youtube.com/watch?v=zjpDtcHS7Sg" - }, - "model": "l8pr.item", - "pk": 405 - }, - { - "fields": { - "added": "2016-06-28T21:15:34.337Z", - "author_name": "Hits70s", - "description": null, - "duration": 254, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=dY9PY4r83p8\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/dY9PY4r83p8/hqdefault.jpg", - "title": "Serge Gainsbourg & Brigitte Bardot - Bonnie And Clyde (1968)", - "updated": "2016-06-28T21:15:34.337Z", - "url": "https://www.youtube.com/watch?v=dY9PY4r83p8" - }, - "model": "l8pr.item", - "pk": 406 - }, - { - "fields": { - "added": "2016-06-28T21:15:34.418Z", - "author_name": "Mildy Forbes", - "description": null, - "duration": 313, - "html": "\n
\n
\n \n
\n \n
\n\n \n \n \n \n \n \n
SoundCloud\n https://soundcloud.com/mildy-forbes/la-decadanse-b\n
\n
\n
\n", - "provider_name": "SoundCloud", - "thumbnail": "http://i1.sndcdn.com/artworks-000002141698-a9hhp3-t500x500.jpg", - "title": "La d\u00e9cadanse by Serge Gainsbourg by Mildy Forbes", - "updated": "2016-06-28T21:15:34.418Z", - "url": "https://soundcloud.com/mildy-forbes/la-decadanse-by-serge-gainsbourg" - }, - "model": "l8pr.item", - "pk": 407 - }, - { - "fields": { - "added": "2016-06-28T21:15:34.469Z", - "author_name": "asefthukom", - "description": null, - "duration": 134, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=iSA8wIGaENA\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/iSA8wIGaENA/hqdefault.jpg", - "title": "Gainsbourg - All the things you are", - "updated": "2016-06-28T21:15:34.470Z", - "url": "https://www.youtube.com/watch?v=iSA8wIGaENA" - }, - "model": "l8pr.item", - "pk": 408 - }, - { - "fields": { - "added": "2016-06-28T21:15:34.522Z", - "author_name": "WhynotPossible", - "description": null, - "duration": 722, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=yACQ2gbndvQ\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/yACQ2gbndvQ/hqdefault.jpg", - "title": "Serge Gainsbourg - Interview - 1968", - "updated": "2016-06-28T21:15:34.522Z", - "url": "https://www.youtube.com/watch?v=yACQ2gbndvQ" - }, - "model": "l8pr.item", - "pk": 409 - }, - { - "fields": { - "added": "2016-06-28T21:15:34.559Z", - "author_name": "TAKE A DRAG OR TWO", - "description": null, - "duration": 199, - "html": "\n
\n
\n \n
\n \n
\n\n \n \n \n \n \n \n
SoundCloud\n https://soundcloud.com/take-a-drag-or-two/serge-ga\n
\n
\n
\n", - "provider_name": "SoundCloud", - "thumbnail": "http://a1.sndcdn.com/images/fb_placeholder.png?1447352542", - "title": "Serge Gainsbourg and Jane Birkin- 69 Anne\u0301e E\u0301rotique by TAKE A DRAG OR TWO", - "updated": "2016-06-28T21:15:34.559Z", - "url": "https://soundcloud.com/take-a-drag-or-two/serge-gainsbourg-and-jane-birkin-69-annee-erotique" - }, - "model": "l8pr.item", - "pk": 410 - }, - { - "fields": { - "added": "2016-06-28T21:15:34.599Z", - "author_name": "Decades", - "description": null, - "duration": 288, - "html": "\n
\n
\n \n
\n \n
\n\n \n \n \n \n \n \n
SoundCloud\n https://soundcloud.com/decades/serge-gainsborough-\n
\n
\n
\n", - "provider_name": "SoundCloud", - "thumbnail": "http://i1.sndcdn.com/artworks-000050638195-x1jtlk-t500x500.jpg", - "title": "Jane Birkin et Serge Gainsbourg - Je T'aime,...Moi Non Plus dL edit by Decades", - "updated": "2016-06-28T21:15:34.599Z", - "url": "https://soundcloud.com/decades/serge-gainsborough-je-taime" - }, - "model": "l8pr.item", - "pk": 411 - }, - { - "fields": { - "added": "2016-06-28T21:15:34.623Z", - "author_name": "Gainsbarr\u00e924", - "description": null, - "duration": 133, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=C11QMm02kLY\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/C11QMm02kLY/hqdefault.jpg", - "title": "SERGE GAINSBOURG NY USA rare version 67", - "updated": "2016-06-28T21:15:34.623Z", - "url": "https://www.youtube.com/watch?v=C11QMm02kLY" - }, - "model": "l8pr.item", - "pk": 412 - }, - { - "fields": { - "added": "2016-06-28T21:15:34.648Z", - "author_name": "pollutiongirl", - "description": null, - "duration": 183, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=seLS8M3hK-c\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/seLS8M3hK-c/hqdefault.jpg", - "title": "Beck & Jane Birkin - L'anamour", - "updated": "2016-06-28T21:15:34.648Z", - "url": "https://www.youtube.com/watch?v=seLS8M3hK-c" - }, - "model": "l8pr.item", - "pk": 413 - }, - { - "fields": { - "added": "2016-06-28T21:15:34.685Z", - "author_name": "John Keats", - "description": null, - "duration": 147, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=OaX9srZVweA\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/OaX9srZVweA/hqdefault.jpg", - "title": "Gainsbourg La javanaise", - "updated": "2016-06-28T21:15:34.685Z", - "url": "https://www.youtube.com/watch?v=OaX9srZVweA" - }, - "model": "l8pr.item", - "pk": 414 - }, - { - "fields": { - "added": "2016-06-28T21:15:34.717Z", - "author_name": "Fran\u00e7ois Miet", - "description": null, - "duration": 123, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=cIDuE4U_fZg\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/cIDuE4U_fZg/hqdefault.jpg", - "title": "Serge Gainsbourg - Ballade de Melody Nelson", - "updated": "2016-06-28T21:15:34.717Z", - "url": "https://www.youtube.com/watch?v=cIDuE4U_fZg" - }, - "model": "l8pr.item", - "pk": 415 - }, - { - "fields": { - "added": "2016-06-28T21:15:34.743Z", - "author_name": "Didier Torche", - "description": null, - "duration": 184, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=IzuTdVJG-ck\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/IzuTdVJG-ck/hqdefault.jpg", - "title": "Serge Gainsbourg la chanson de Pr\u00e9vert", - "updated": "2016-06-28T21:15:34.743Z", - "url": "https://www.youtube.com/watch?v=IzuTdVJG-ck" - }, - "model": "l8pr.item", - "pk": 416 - }, - { - "fields": { - "added": "2016-06-28T21:15:34.770Z", - "author_name": "7173Productions", - "description": null, - "duration": 91, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=Qqfd5xZZ30E\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/Qqfd5xZZ30E/hqdefault.jpg", - "title": "Slogan (1969) trailer", - "updated": "2016-06-28T21:15:34.770Z", - "url": "https://www.youtube.com/watch?v=Qqfd5xZZ30E" - }, - "model": "l8pr.item", - "pk": 417 - }, - { - "fields": { - "added": "2016-06-28T21:15:34.798Z", - "author_name": "Cristian Trujillo", - "description": null, - "duration": 213, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=VPOYtC1n5bE\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/VPOYtC1n5bE/hqdefault.jpg", - "title": "Initials BB - Serge Gainsbourg", - "updated": "2016-06-28T21:15:34.798Z", - "url": "https://www.youtube.com/watch?v=VPOYtC1n5bE" - }, - "model": "l8pr.item", - "pk": 418 - }, - { - "fields": { - "added": "2016-06-28T21:15:34.828Z", - "author_name": "vse vyshe", - "description": null, - "duration": 3, - "html": "\n
\n
\n \n
\n \n\n\n
\n\n \n \n \n \n \n \n
YouTube\n https://www.youtube.com/watch?v=E8ZCvYg5-ZQ\n
\n
\n
\n", - "provider_name": "YouTube", - "thumbnail": "https://i.ytimg.com/vi/E8ZCvYg5-ZQ/hqdefault.jpg", - "title": "Serge Gainsbourg, Le Poin\u00e7onneur des Lilas, 1959", - "updated": "2016-06-28T21:15:34.828Z", - "url": "https://www.youtube.com/watch?v=E8ZCvYg5-ZQ" - }, - "model": "l8pr.item", - "pk": 419 - } -] diff --git a/app/l8pr/migrations/0023_auto_20170405_1342.py b/app/l8pr/migrations/0023_auto_20170405_1342.py new file mode 100644 index 0000000..eed83f9 --- /dev/null +++ b/app/l8pr/migrations/0023_auto_20170405_1342.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2017-04-05 13:42 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('l8pr', '0022_show_show_type'), + ] + + operations = [ + migrations.CreateModel( + name='ItemsUsersRelationship', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('added', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='l8pr.Item')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ('-added',), + }, + ), + migrations.AddField( + model_name='item', + name='users', + field=models.ManyToManyField(related_name='ItemsUsersRelationship', through='l8pr.ItemsUsersRelationship', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/app/l8pr/migrations/0024_auto_20170413_1653.py b/app/l8pr/migrations/0024_auto_20170413_1653.py new file mode 100644 index 0000000..623b7dd --- /dev/null +++ b/app/l8pr/migrations/0024_auto_20170413_1653.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-04-13 16:53 +from __future__ import unicode_literals + +from django.db import migrations + + +def link_items_to_users(apps, schema_editor): + Show = apps.get_model('l8pr', 'Show') + ItemsUsersRelationship = apps.get_model('l8pr', 'ItemsUsersRelationship') + for s in Show.objects.all(): + for i in s.items.all(): + if s.user not in i.users.all(): + rel = ItemsUsersRelationship(item=i, user=s.user) + rel.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('l8pr', '0023_auto_20170405_1342'), + ] + + operations = [ + migrations.RunPython(link_items_to_users), + ] diff --git a/app/l8pr/migrations/0025_auto_20170421_2302.py b/app/l8pr/migrations/0025_auto_20170421_2302.py new file mode 100644 index 0000000..99ffa21 --- /dev/null +++ b/app/l8pr/migrations/0025_auto_20170421_2302.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-04-21 23:02 +from __future__ import unicode_literals + +from django.db import migrations + + +def remove_empty_title_and_provider(apps, schema_editor): + Item = apps.get_model('l8pr', 'Item') + for item in Item.objects.all(): + if not item.title or not item.provider_name: + item.delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('l8pr', '0024_auto_20170413_1653'), + ] + + operations = [ + migrations.RunPython(remove_empty_title_and_provider), + ] diff --git a/app/l8pr/migrations/0026_auto_20170503_0803.py b/app/l8pr/migrations/0026_auto_20170503_0803.py new file mode 100644 index 0000000..5b7769a --- /dev/null +++ b/app/l8pr/migrations/0026_auto_20170503_0803.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-05-03 08:03 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('l8pr', '0025_auto_20170421_2302'), + ] + + operations = [ + migrations.RemoveField( + model_name='itemsusersrelationship', + name='item', + ), + migrations.RemoveField( + model_name='itemsusersrelationship', + name='user', + ), + migrations.RemoveField( + model_name='item', + name='users', + ), + migrations.DeleteModel( + name='ItemsUsersRelationship', + ), + ] diff --git a/app/l8pr/migrations/0027_item_shows.py b/app/l8pr/migrations/0027_item_shows.py new file mode 100644 index 0000000..31be71f --- /dev/null +++ b/app/l8pr/migrations/0027_item_shows.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-05-03 08:09 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('l8pr', '0026_auto_20170503_0803'), + ] + + operations = [ + migrations.AddField( + model_name='item', + name='shows', + field=models.ManyToManyField(related_name='ItemsRelationship', through='l8pr.ItemsRelationship', to='l8pr.Show'), + ), + ] diff --git a/app/l8pr/migrations/0028_auto_20170503_1004.py b/app/l8pr/migrations/0028_auto_20170503_1004.py new file mode 100644 index 0000000..1281c77 --- /dev/null +++ b/app/l8pr/migrations/0028_auto_20170503_1004.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-05-03 10:04 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('l8pr', '0027_item_shows'), + ] + + operations = [ + migrations.AddField( + model_name='itemsrelationship', + name='added', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name='itemsrelationship', + name='updated', + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/app/l8pr/migrations/0029_profile_follows.py b/app/l8pr/migrations/0029_profile_follows.py new file mode 100644 index 0000000..dff7cff --- /dev/null +++ b/app/l8pr/migrations/0029_profile_follows.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-05-29 12:29 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('l8pr', '0028_auto_20170503_1004'), + ] + + operations = [ + migrations.AddField( + model_name='profile', + name='follows', + field=models.ManyToManyField(blank=True, related_name='followers', to='l8pr.Profile'), + ), + ] diff --git a/app/l8pr/migrations/0030_auto_20170529_1357.py b/app/l8pr/migrations/0030_auto_20170529_1357.py new file mode 100644 index 0000000..d0ca59d --- /dev/null +++ b/app/l8pr/migrations/0030_auto_20170529_1357.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-05-29 13:57 +from __future__ import unicode_literals + +from django.db import migrations + + +def create_profile(apps, schema_editor): + Profile = apps.get_model('l8pr', 'Profile') + User = apps.get_model('auth', 'User') + for user in User.objects.all(): + Profile.objects.get_or_create(user=user) + + +class Migration(migrations.Migration): + + dependencies = [ + ('l8pr', '0029_profile_follows'), + ] + + operations = [ + migrations.RunPython(create_profile), + ] diff --git a/app/l8pr/models.py b/app/l8pr/models.py index c3ca9e5..3ed2e49 100644 --- a/app/l8pr/models.py +++ b/app/l8pr/models.py @@ -20,6 +20,10 @@ class Profile(models.Model): user = models.OneToOneField(User, related_name='profile') avatar = models.URLField(max_length=500, null=True, blank=True) + follows = models.ManyToManyField('Profile', related_name='followers', blank=True) + + def __str__(self): + return '%s profile' % self.user.__str__() class Loop(models.Model): @@ -103,6 +107,8 @@ class ItemsRelationship(models.Model): item = models.ForeignKey('Item') show = models.ForeignKey('Show') order = models.PositiveIntegerField() + added = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) class Meta: ordering = ('-order',) @@ -119,6 +125,7 @@ class Item(models.Model): ('Vimeo', 'Vimeo'), ) title = models.CharField(max_length=255, null=True, blank=True) + shows = models.ManyToManyField('Show', through='ItemsRelationship', related_name='ItemsRelationship') description = models.TextField(null=True, blank=True) author_name = models.CharField(max_length=255, null=True, blank=True) thumbnail = models.URLField(max_length=200, null=True, blank=True) @@ -234,6 +241,7 @@ def completeItem(sender, instance, created, **kwargs): if instance.duration: instance.save() + signals.post_save.connect(completeItem, sender=Item) @@ -241,6 +249,7 @@ def create_show_settings(sender, instance, created, **kwargs): if created and not getattr(instance, 'settings', None): ShowSettings.objects.create(show=instance) + signals.post_save.connect(create_show_settings, sender=Show) diff --git a/app/l8pr/permissions.py b/app/l8pr/permissions.py new file mode 100644 index 0000000..8cb001b --- /dev/null +++ b/app/l8pr/permissions.py @@ -0,0 +1,16 @@ +from rest_framework import permissions + + +class IsOwnerOrReadOnly(permissions.BasePermission): + """ + Object-level permission to only allow owners of an object to edit it. + Assumes the model instance has an `owner` attribute. + """ + + def has_object_permission(self, request, view, obj): + # Read permissions are allowed to any request, + # so we'll always allow GET, HEAD or OPTIONS requests. + if request.method in permissions.SAFE_METHODS: + return True + + return obj.user == request.user diff --git a/app/l8pr/search_indexes.py b/app/l8pr/search_indexes.py index 968bcba..abbfa4d 100644 --- a/app/l8pr/search_indexes.py +++ b/app/l8pr/search_indexes.py @@ -1,45 +1,40 @@ -import datetime +from django.db import models from haystack import indexes -from .models import Item, Show +from .models import Item, Show, ItemsRelationship +from django.contrib.auth.models import User class ItemIndex(indexes.SearchIndex, indexes.Indexable): - text = indexes.CharField(document=True) + text = indexes.CharField(document=True, use_template=True) url = indexes.CharField(model_attr='url') - provider_name = indexes.CharField(model_attr='provider_name') - title = indexes.CharField(model_attr='title') + provider_name = indexes.CharField(model_attr='provider_name', null=True) + title = indexes.CharField(model_attr='title', null=True) author_name = indexes.CharField(model_attr='author_name', null=True) thumbnail = indexes.CharField(model_attr='thumbnail', null=True) - autocomplete = indexes.EdgeNgramField() def get_model(self): return Item - @staticmethod - def prepare_autocomplete(obj): - return " ".join(( - obj.title, obj.author_name or '' - )) - - def index_queryset(self, using=None): - return self.get_model().objects.filter( - added__lte=datetime.datetime.now() - ) - class ShowIndex(indexes.SearchIndex, indexes.Indexable): - text = indexes.CharField(document=True) + text = indexes.CharField(document=True, model_attr='title') title = indexes.CharField(model_attr='title') - autocomplete = indexes.EdgeNgramField() def get_model(self): return Show - @staticmethod - def prepare_autocomplete(obj): - return obj.title - def index_queryset(self, using=None): - return self.get_model().objects.filter( - added__lte=datetime.datetime.now() - ) +class UserIndex(indexes.SearchIndex, indexes.Indexable): + text = indexes.CharField(document=True, model_attr='username') + username = indexes.CharField(model_attr='username') + + def get_model(self): + return User + + +def reindex_mymodel(sender, **kwargs): + ItemIndex().update_object(kwargs['instance'].item) + ShowIndex().update_object(kwargs['instance'].show) + + +models.signals.post_save.connect(reindex_mymodel, sender=ItemsRelationship) diff --git a/app/l8pr/static/actions/auth.js b/app/l8pr/static/actions/auth.js new file mode 100644 index 0000000..8eeb470 --- /dev/null +++ b/app/l8pr/static/actions/auth.js @@ -0,0 +1,193 @@ +import fetch from 'isomorphic-fetch' +import { push } from 'react-router-redux' +import * as api from '../utils/api' +import { hideModal } from '../actions/modal' +import { orderBy, cloneDeep } from 'lodash' +import { checkHttpStatus, parseJSON } from '../utils' +import SERVER_URL from '../utils/config' +import { + AUTH_LOGIN_USER_REQUEST, + AUTH_LOGIN_USER_FAILURE, + AUTH_LOGIN_USER_SUCCESS, + AUTH_LOGOUT_USER, +} from '../constants' + + +export function updateUser(user) { + return { + type: 'AUTH_UPDATE_USER', + payload: user, + } +} +export function follow(userId) { + return (dispatch, getState) => { + api.follow(getState(), userId) + .then((user) => { + dispatch(updateUser(user)) + }) + } +} +export function unfollow(userId) { + return (dispatch, getState) => { + api.unfollow(getState(), userId) + .then((user) => { + dispatch(updateUser(user)) + }) + } +} +export function authLoginUserSuccess(token, user) { + return (dispatch, getState) => { + dispatch({ + type: AUTH_LOGIN_USER_SUCCESS, + payload: { + token, + user, + }, + }) + // get user's shows + api.fetchUserShows(getState(), { username: user.username }) + .then((loop) => { + dispatch({ + type: 'AUTH_SET_LOOP', + payload: orderBy(loop, ['updated'], ['desc']), + }) + }) + } +} + +export function saveItem(item) { + return (dispatch, getState) => ( + api.saveItem(getState(), { ...item }) + ) +} + +export function saveShow(show) { + return (dispatch, getState) => ( + Promise.all(show.items.map((item) => dispatch(saveItem(item)))) + .then(items => ({ + ...show, + items, + })) + .then(show => ( + api.saveShow(getState(), show) + // update show + .then((show) => dispatch(updateShow(show))) + )) + ) +} + +export function toggleItemToShow(item, show) { + return (dispatch) => ( + // save item + dispatch(saveItem(item)) + // save or remove the item in the show + .then((item) => { + const newShow = cloneDeep(show) + if (newShow.items.find(i => i.id === item.id)) { + newShow.items = newShow.items.filter((i) => (i.id !== item.id)) + } else { + newShow.items = [item, ...newShow.items] + } + return dispatch(saveShow(newShow)) + .then(() => (item)) + }) + ) +} + +export function updateShow(show) { + return { + type: 'AUTH_UPDATE_SHOW', + payload: show, + } +} + +export function authLoginUserFailure(error, message) { + return { + type: AUTH_LOGIN_USER_FAILURE, + payload: { + status: error, + statusText: message, + }, + } +} + +export function authLoginUserRequest() { + return { type: AUTH_LOGIN_USER_REQUEST } +} + +export function authLogout() { + return { type: AUTH_LOGOUT_USER } +} + +export function authLogoutAndRedirect() { + return (dispatch) => { + dispatch(authLogout()) + dispatch(push('/login')) + return Promise.resolve() // TODO: we need a promise here because of the tests, find a better way + } +} + +export function checkToken(token) { + return (dispatch) => { + if (!token) { + return Promise.resolve(false) + } + return fetch(`${SERVER_URL}/api/users/me/`, { + method: 'get', + headers: { Authorization: `Token ${token}` }, + }) + .then(checkHttpStatus) + .catch(() => { + dispatch(authLogout()) + }) + .then(parseJSON) + .then((user) => ( + dispatch(authLoginUserSuccess(token, user)) + )) + } +} + +export function authLoginUser(email, password, redirect = '/') { + return (dispatch) => { + dispatch(authLoginUserRequest()) + var formData = new FormData() + formData.append('password', password) + formData.append('username', email) + return fetch(`${SERVER_URL}/auth/login/`, { + method: 'post', + body: formData, + headers: { Accept: 'application/json' }, + }) + .then(checkHttpStatus) + .then(parseJSON) + .then((response) => ( + fetch(`${SERVER_URL}/api/users/me/`, { + method: 'get', + headers: { Authorization: `Token ${response.auth_token}` }, + }) + .then(checkHttpStatus) + .then(parseJSON) + .then((user) => { + dispatch(authLoginUserSuccess(response.auth_token, user)) + dispatch(hideModal()) + dispatch(push(redirect)) + }) + )) + .catch((error) => { + if (error && typeof error.response !== 'undefined' && error.response.status === 401) { + // Invalid authentication credentials + return error.response.json().then((data) => { + dispatch(authLoginUserFailure(401, data.non_field_errors[0])) + }) + } else if (error && typeof error.response !== 'undefined' && error.response.status >= 500) { + // Server side error + dispatch(authLoginUserFailure(500, 'A server error occurred while sending your data!')) + } else { + // Most likely connection issues + dispatch(authLoginUserFailure('Connection Error', 'An error occurred while sending your data!')) + } + + return Promise.resolve() // TODO: we need a promise here because of the tests, find a better way + }) + } +} diff --git a/app/l8pr/static/actions/browser.js b/app/l8pr/static/actions/browser.js new file mode 100644 index 0000000..7602a57 --- /dev/null +++ b/app/l8pr/static/actions/browser.js @@ -0,0 +1,85 @@ +import * as c from '../constants' +import * as api from '../utils/api' +import * as data from '../actions/data' + +export function openContext(contextId) { + return { + type: 'BROWSER_OPEN_CONTEXT', + payload: contextId, + } +} + +export function openStrip() { + return { + type: c.SET_STRIP_STATE, + payload: { + stripOpened: true, + stripHidden: false, + }, + } +} +export function closeStrip() { + return { + type: c.SET_STRIP_STATE, + payload: { stripOpened: false }, + } +} + +export function toggleFixedStrip() { + return (dispatch, getState) => { + dispatch({ + type: c.SET_STRIP_STATE, + payload: { stripFixed: !getState().browser.stripFixed }, + }) + } +} + +export function toggleStrip(browserType, browserProps) { + return (dispatch, getState) => { + function toggle() { + if (getState().browser.stripOpened) { + dispatch(closeStrip()) + } else { + dispatch(openStrip()) + } + } + if (!browserType) { + toggle() + } else { + if (browserType === getState().browser.browserType) { + toggle() + } else { + dispatch(browse(browserType, browserProps)) + } + } + } +} +export function browse(browserType, browserProps) { + return { + type: c.BROWSE, + browserType, + browserProps, + } +} + +export function browseShow(showIdOrObject) { + return (dispatch, getState) => { + if (typeof showIdOrObject === 'object') { + return dispatch(browse('SHOW', { show: showIdOrObject })) + } + if (getState().data.shows[showIdOrObject]) { + return dispatch(browse('SHOW', { show: getState().data.shows[showIdOrObject] })) + } else { + return api.show(getState(), { id: showIdOrObject }) + .then(show => { + dispatch(data.addShows([show])) + dispatch(browse('SHOW', { show })) + }) + } + } + +} + +export function goBack() { + return { type: 'BROWSER_GO_BACK' } +} diff --git a/app/l8pr/static/actions/data.js b/app/l8pr/static/actions/data.js new file mode 100644 index 0000000..a0d277d --- /dev/null +++ b/app/l8pr/static/actions/data.js @@ -0,0 +1,91 @@ +import * as api from '../utils/api' + +export const addShows = (shows) => ({ + type: 'ADD_SHOWS', + payload:shows, +}) + +export const feed = () => { + return (dispatch, getState) => ( + api.feed(getState()) + .then((items) => { + return items.map((i) => ( + { + ...i, + context: { + id: 'feed', + title: 'feed', + }, + } + )) + }) + ) +} + +export const lastItemsInLoopr = () => { + return (dispatch, getState) => ( + api.lastItemsInLoopr(getState()) + // set a context + .then((items) => { + const show = { + title: 'Last items in loopr', + id: 'last_items_in_loopr', + } + dispatch(addShows([{ + ...show, + items: items, + }])) + return items.map((i) => ( + { + ...i, + context: show, + } + )) + }) + ) +} + +export const shows = ({ username }) => ( + (dispatch, getState) => ( + api.fetchUserShows(getState(), { username }) + .then((shows) => { + dispatch(addShows(shows)) + return shows + }) + .then((shows) => ( + shows.map((show) => ( + // set context + show.items.map((i) => ({ + ...i, + context: { + ...show, + items: null, + }, + })) + )) + )) + // flatten items + .then((showsItems) => ([].concat.apply([], showsItems))) + ) +) + +export const lastUserItems = ({ username }) => { + const show = { + title: 'Last Items', + id: 'last_item', + } + return (dispatch, getState) => ( + api.lastUserItems(getState(), { username }) + .then((items) => { + dispatch(addShows([{ + ...show, + items: items, + }])) + // set context + return items.map((i) => ({ + ...i, + context: show, + })) + }) + ) +} diff --git a/app/l8pr/static/actions/modal.js b/app/l8pr/static/actions/modal.js new file mode 100644 index 0000000..1244de3 --- /dev/null +++ b/app/l8pr/static/actions/modal.js @@ -0,0 +1,6 @@ +export const showModal = (modalType, modalProps) => ({ + type: 'SHOW_MODAL', + modalType, + modalProps, +}) +export const hideModal = () => ({ type: 'HIDE_MODAL' }) diff --git a/app/l8pr/static/actions/player.js b/app/l8pr/static/actions/player.js new file mode 100644 index 0000000..57ec321 --- /dev/null +++ b/app/l8pr/static/actions/player.js @@ -0,0 +1,161 @@ +import * as c from '../constants' +import * as selectors from '../selectors' +import { push } from 'react-router-redux' +import * as data from '../actions/data' + +export function setPlaylist(playlist) { + return { + type: c.SET_PLAYLIST, + payload: playlist, + } +} + +export function appendToPlaylist(items) { + return { + type: c.APPEND_TO_PLAYLIST, + payload: items, + } +} + +export function insertToPlaylist(items) { + return { + type: c.INSERT_TO_PLAYLIST, + payload: items, + } +} + +export function fillQueueList() { + return (dispatch) => ( + dispatch(data.lastItemsInLoopr()) + .then((items) => ( + dispatch(appendToPlaylist(items)) + )) + ) +} + +export function initQueueList({ username, show, item }) { + return (dispatch, getState) => { + return Promise.all([ + // LAST USER ITEMS + dispatch(data.lastUserItems({ username })), + // SHOWS + dispatch(data.shows({ username })), + ]) + // flatten + .then((results) => ([].concat.apply([], results))) + // add to playlist + .then((items) => (dispatch(setPlaylist(items)))) + // set the current item + .then(() => dispatch(next())) + // play the first occurence of the asked item + .then(() => { + if (item) { + const itemToPlay = selectors.playlist(getState()).find( + (i) => (i.id.toString() === item.toString()) + ) + if (itemToPlay) { + dispatch(playItem(itemToPlay)) + } + } + }) + // start the player + .then(() => dispatch(play())) + } +} + +export function jumpToItem(item) { + return { + type: c.JUMP_TO_ITEM, + payload: item, + } +} + +export function playItem(item) { + return playItems([item]) +} + +export function playItems(items) { + return (dispatch) => { + dispatch(insertToPlaylist(items)) + dispatch(next()) + } +} + +function addTrackAsPlayed(track) { + return () => { + // update history in localstorage + const playedTracks = JSON.parse(localStorage.getItem('playedTracks')) || [] + if (playedTracks.indexOf(track.id) === -1) { + playedTracks.push(track.id) + localStorage.setItem('playedTracks', JSON.stringify(playedTracks)) + } + } +} +export function play() { + return (dispatch, getState) => { + dispatch({ type: c.PLAY }) + const show = selectors.currentShow(getState()) + const item = selectors.currentTrack(getState()) + // update already played tracks + dispatch(addTrackAsPlayed(item)) + // update url + let url = '' + if (show) { + url += `/show/${show.id}` + } if (item) { + url += `/item/${item.id}` + } + dispatch(push(url)) + if (selectors.getPlaylistGroupedByContext(getState()).length < 2) { + dispatch(fillQueueList()) + } + } +} + +export function togglePlay() { + return (dispatch, getState) => { + if (getState().player.playing) { + dispatch(pause()) + } else { + dispatch(play()) + } + } +} + +export function toggleMute() { + return (dispatch, getState) => { + if (getState().player.muted) { + dispatch(unmute()) + } else { + dispatch(mute()) + } + } +} + +export function previousContext() { + return { type: c.PREVIOUS_CONTEXT } +} + +export function nextContext() { + return { type: c.NEXT_CONTEXT } +} + +export function next() { + return { type: c.NEXT } +} + +export function previous() { + return { type: c.PREVIOUS } +} + +export function mute() { + return { type: c.MUTE } +} + +export function unmute() { + return { type: c.UNMUTE } +} + +export function pause() { + return { type: c.PAUSE } +} diff --git a/app/l8pr/static/actions/search.js b/app/l8pr/static/actions/search.js new file mode 100644 index 0000000..8097096 --- /dev/null +++ b/app/l8pr/static/actions/search.js @@ -0,0 +1,87 @@ +import * as api from '../utils/api' +import * as c from '../constants' +import * as data from '../actions/data' +import * as selectors from '../selectors' + +var lastSearch = undefined + +export function search(searchTerms) { + const timestamp = Date.now() + lastSearch = timestamp + return (dispatch, getState) => { + dispatch({ type: 'SEARCH_STARTING' }) + dispatch(setTerms([...searchTerms])) + if (searchTerms.length) { + const preSearch = [] + const urls = [] + const users = [] + const keywords = [] + searchTerms.forEach((s) => { + // find urls + if (s.value.match(/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/gi)) { + urls.push(s) + } else if (s.value === '#feed') { + preSearch.push(dispatch(data.feed())) + } else if (s.value === '#my-last-tracks') { + preSearch.push(dispatch(data.lastUserItems({ username: selectors.currentUsername(getState()) }))) + } else if (s.value.startsWith('@')) { + users.push(s.value.slice(1)) + } else { + keywords.push(s) + } + }) + const urlsMeta = Promise.all(urls.map((u) => (api.metadata(getState(), u.value)))) + const usersShowsResults = Promise.all(users.map(username => (api.fetchUserShows(getState(), { username })))) + .then((usersShows) => ([].concat.apply([], usersShows))) + const termsResults = keywords.length && api.search(getState(), keywords) + const youtubeResult = keywords.length && api.youtube(getState(), keywords) + .then((results) => (results.map((r) => ({ + ...r, + context: { + title: 'youtube search', + id: 'youtube_search', + } + })))) + Promise.all([ + ...preSearch, + urlsMeta, + usersShowsResults, + termsResults, + youtubeResult, + ].map(p => (Promise.resolve(p)))) + .then((results) => { + if (timestamp === lastSearch) { + dispatch(setList( + results + .reduce((a, b) => (a.concat(b)), []) + .filter(i => (i)) + )) + } + }) + } else { + dispatch(clearList()) + + } + } +} + +function setList(items) { + return { + type: c.SET_SEARCH_RESULT, + payload: items, + } +} + +function clearList(items) { + return { + type: c.CLEAR_SEARCH_LIST, + payload: items, + } +} + +function setTerms(terms) { + return { + type: c.SET_SEARCH_TERMS, + payload: terms, + } +} diff --git a/app/l8pr/static/api.service.js b/app/l8pr/static/api.service.js deleted file mode 100644 index bc9262a..0000000 --- a/app/l8pr/static/api.service.js +++ /dev/null @@ -1,139 +0,0 @@ -(function() { - 'use strict'; - - Api.$inject = ['Restangular']; - function Api(Restangular) { - var self = { - Accounts: Restangular.service('users'), - Auth: Restangular.service('auth'), - Register: Restangular.service('register'), - Items: Restangular.service('items'), - Search: Restangular.service('search'), - SearchYoutube: Restangular.service('youtube'), - GetItemMetadata: Restangular.service('metadata'), - Shows: (function() { - Restangular.extendModel('shows', function(model) { - model.duration = function() { - if (angular.isDefined(model.items)) { - return model.items.reduce(function(a, b) {return a + b.duration;}, 0); - } - }; - model.listTypes = function() { - var types = model.items.map(function(link) { - return link.provider_name; - }); - if (_.contains(types, 'SoundCloud') && model.settings.giphy) { - types.push('Giphy'); - } - return _.unique(types); - }; - return model; - }); - return Restangular.service('shows'); - })(), - LatestItems: function() { - var items = []; - return self.Shows.getList({ordering: '-updated', limit: 10}).then(function(shows) { - shows.forEach(function(show) { - show.items.slice(0, 5).forEach(function(item) { - item.show = angular.copy(show); - items.push(item); - }); - }); - return items; - }); - }, - Loops: (function() { - Restangular.extendModel('loops', function(model) { - if (angular.isDefined(model.shows_list)) { - model.shows_list = model.shows_list.map(function(show) { - return Restangular.restangularizeElement(null, show, 'shows'); - }); - } - model.duration = function() { - if (angular.isDefined(model.shows_list)) { - return model.shows_list.reduce(function(a, b) {return a + b.duration();}, 0); - } - }; - return model; - }); - return Restangular.service('loops'); - })(), - FindOrCreateItem: (function() { - return function(item) { - if (item.id) { - return item; - } - return self.Items.getList({url: item.url}).then(function(items) { - if (items.length === 0) { - return self.Items.post({url: item.url}).then(function(item) { - return item; - }); - } - return items[0]; - }); - }; - })(), - FindOrCreateInbox: (function() { - return function(params) { - return self.Shows.getList({show_type: 'inbox', user: params.user}).then(function(shows) { - var show; - if (shows.length === 0) { - show = angular.extend({}, { - show_type: 'inbox', - title: 'my inbox', - items: [] - }, show); - return self.Shows.post(show).then(function(show) { - return show; - }); - } - if (shows.length > 1) { - throw {message: 'We found more than 1 inbox', params: params}; - } - return shows[0]; - }); - }; - })() - }; - return self; - } - - angular.module('loopr.api', ['restangular', 'LocalStorageModule']) - .config(['localStorageServiceProvider', function(localStorageServiceProvider) { - localStorageServiceProvider - .setPrefix('loopr') - .setStorageType('localStorage') - .setNotify(true, true); - }]) - .factory('Api', Api) - .config(['RestangularProvider', function(RestangularProvider) { - RestangularProvider.setBaseUrl('/api'); - RestangularProvider.setRequestSuffix('/'); - RestangularProvider.addResponseInterceptor(function(data, operation, what, url, response, deferred) { - var extractedData = data; - // .. to look for getList operations - if (operation === 'getList' && !Array.isArray(extractedData) && extractedData.results) { - extractedData = extractedData.results; - } - return extractedData; - }); - // X-CSRFToken - function getCookie(name) { - var cookieValue = null; - if (document.cookie && document.cookie !== '') { - var cookies = document.cookie.split(';'); - for (var i = 0; i < cookies.length; i++) { - var cookie = $.trim(cookies[i]); - // Does this cookie string begin with the name we want? - if (cookie.substring(0, name.length + 1) === (name + '=')) { - cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); - break; - } - } - } - return cookieValue; - } - RestangularProvider.setDefaultHeaders({'X-CSRFToken': getCookie('csrftoken')}); - }]); -})(); diff --git a/app/l8pr/static/app.js b/app/l8pr/static/app.js new file mode 100644 index 0000000..196de52 --- /dev/null +++ b/app/l8pr/static/app.js @@ -0,0 +1,54 @@ +import React from 'react' +import { Link } from 'react-router' +import { connect } from 'react-redux' +import { push } from 'react-router-redux' +import classNames from 'classnames' + +import { authLogoutAndRedirect } from './actions/auth' +import './styles/main.scss' + +class App extends React.Component { + + static propTypes = { + isAuthenticated: React.PropTypes.bool.isRequired, + children: React.PropTypes.shape().isRequired, + dispatch: React.PropTypes.func.isRequired, + pathName: React.PropTypes.string.isRequired, + }; + + logout = () => { + this.props.dispatch(authLogoutAndRedirect()) + }; + + goToIndex = () => { + this.props.dispatch(push('/')) + }; + + goToProtected = () => { + this.props.dispatch(push('/protected')) + }; + + render() { + const homeClass = classNames({ active: this.props.pathName === '/' }) + const protectedClass = classNames({ active: this.props.pathName === '/protected' }) + const loginClass = classNames({ active: this.props.pathName === '/login' }) + + return ( +
+
+ {this.props.children} +
+
+ ) + } +} + +const mapStateToProps = (state, ownProps) => { + return { + isAuthenticated: state.auth.isAuthenticated, + pathName: ownProps.location.pathname, + } +} + +export default connect(mapStateToProps)(App) +export { App as AppNotConnected } diff --git a/app/l8pr/static/components/AddItemModal/index.js b/app/l8pr/static/components/AddItemModal/index.js new file mode 100644 index 0000000..b8c3b7c --- /dev/null +++ b/app/l8pr/static/components/AddItemModal/index.js @@ -0,0 +1,80 @@ +import React from 'react' +import { Modal, Button } from 'react-bootstrap' +import QuickAddShow from '../QuickAddShow' +import { connect } from 'react-redux' +import * as auth from '../../actions/auth' +import * as modal from '../../actions/modal' +import * as selectors from '../../selectors' +import { getDuration } from '../../utils' +import classNames from 'classnames' +import './style.scss' + +function AddItemModal({ handleHide, shows, item, toggleItemToShow, saveItemAndCreateShow }) { + return ( + + + + + +

Add the track to a show

+
+ +
+ saveItemAndCreateShow({ title }, item )} /> + {shows.map((show) => { + let classes = classNames( + 'AddItemModal__item', + { 'AddItemModal__item--contains-item': !!show.items.find((i) => i.id === item.id) } + ) + return ( +
toggleItemToShow(item, show)}> +
{show.title}
+
+ {show.items.length} tracks + {getDuration(show.items)} +
+
+ ) + })} +
+
+ + + +
+ ) +} +AddItemModal.propTypes = { + shows: React.PropTypes.array.isRequired, + item: React.PropTypes.object.isRequired, + handleHide: React.PropTypes.func.isRequired, + toggleItemToShow: React.PropTypes.func.isRequired, + saveItemAndCreateShow: React.PropTypes.func.isRequired, +} +const mapStateToProps = (state) => ({ + shows: selectors.myShows(state), + item: state.modal.modalProps.item, +}) + +const mapDispatchToProps = (dispatch) => ({ + toggleItemToShow: (item, show) => ( + dispatch(auth.toggleItemToShow(item, show)) + .then((item) => ( + dispatch(modal.showModal('ADD_ITEM', { item })) + )) + ), + saveItemAndCreateShow: (show, item) => { + dispatch(auth.saveItem(item)) + .then((item) => { + dispatch(modal.showModal('ADD_ITEM', { item })) + return item + }) + .then(item => ( + dispatch(auth.saveShow({ + ...show, + items: [item], + })) + )) + }, +}) +export default connect(mapStateToProps, mapDispatchToProps)(AddItemModal) diff --git a/app/l8pr/static/components/AddItemModal/style.scss b/app/l8pr/static/components/AddItemModal/style.scss new file mode 100644 index 0000000..23fc187 --- /dev/null +++ b/app/l8pr/static/components/AddItemModal/style.scss @@ -0,0 +1,20 @@ +.AddItemModal__item { + display: flex; + font-family: $primaryFont; + padding: 5px; + cursor: pointer; + margin: 5px 0px; +} + +.AddItemModal__item:hover { + background-color: purple; + color: white; +} + +.AddItemModal__item--contains-item { + background-color: purple; + color: white; +} +.AddItemModal__title, .AddItemModal__details > * { + margin-right: 1rem; +} diff --git a/app/l8pr/static/components/Browser/index.js b/app/l8pr/static/components/Browser/index.js new file mode 100644 index 0000000..1a81cd0 --- /dev/null +++ b/app/l8pr/static/components/Browser/index.js @@ -0,0 +1,29 @@ +import React from 'react' +import { connect } from 'react-redux' +import { PlayQueue, Search, Show } from '../index' +import classNames from 'classnames' +import './style.scss' + + +function Browser({ browserType, open }) { + const types = { + 'SEARCH': Search, + 'PLAYQUEUE': PlayQueue, + 'SHOW': Show, + } + const classes = classNames( + 'Browser', + { 'Browser--open': open }, + ) + const BrowserElement = types[browserType] + return +} + +Browser.propTypes = { + browserType: React.PropTypes.string.isRequired, + open: React.PropTypes.bool, +} + +const mapStateToProps = (state) => ({ browserType: state.browser.browserType }) + +export default connect(mapStateToProps)(Browser) diff --git a/app/l8pr/static/components/Browser/style.scss b/app/l8pr/static/components/Browser/style.scss new file mode 100644 index 0000000..3115899 --- /dev/null +++ b/app/l8pr/static/components/Browser/style.scss @@ -0,0 +1,15 @@ +.Browser { + transition-duration: 1s; + transition-property: all; + height: 0; + overflow: hidden; + position: relative; + display: flex; + flex-direction: column; + background-color: white; + opacity: 0; +} +.Browser--open { + opacity: 1; + height: 55vh; +} diff --git a/app/l8pr/static/components/Context/index.js b/app/l8pr/static/components/Context/index.js new file mode 100644 index 0000000..8640656 --- /dev/null +++ b/app/l8pr/static/components/Context/index.js @@ -0,0 +1,127 @@ +import React from 'react' +import { connect } from 'react-redux' +import * as selectors from '../../selectors' +import './style.scss' +import * as player from '../../actions/player' +import * as browser from '../../actions/browser' +import * as auth from '../../actions/auth' +import * as modal from '../../actions/modal' +import 'react-select/dist/react-select.css' +import { ListItem, ResultsCounter } from '../index' +import classNames from 'classnames' +import 'react-virtualized/styles.css' +import { get } from 'lodash' + +class Context extends React.Component { + render() { + const { + className, + context, + onPlayClick, + onAddClick, + currentTrack, + follow, + unfollow, + playItem, + isFollowingAuthor, + highlightFirstItem, + onOpenClick, + open, + } = this.props + let items + let highlight + let contextBackground + if (highlightFirstItem) { + highlight = context.items[0] + items = context.items.slice(1) + contextBackground = null + } else { + items = context.items + contextBackground = items[0].thumbnail + } + const contextObj = context.context + const classes = classNames( + 'Context', + className, + { 'Context--highlightFirstItem': highlightFirstItem }, + { 'Context--opened': open } + ) + return ( +
+
onOpenClick(contextObj)} + style={{ backgroundImage: contextBackground && `url(${contextBackground})` }} + > +
{contextObj.title}
+ {!highlight && +
+ playItem(items[0])}> + play_circle_outline + +
+ } +
+ {contextObj.user &&
{contextObj.user.username}
} + +
+ {highlight && + + } +
+
+ {open && items.map((item) => ( + playItem(item)}/> + ))} +
+
+ ) + } +} + +Context.propTypes = { + className: React.PropTypes.string, + onAddClick: React.PropTypes.func.isRequired, + onPlayClick: React.PropTypes.func.isRequired, + currentTrack: React.PropTypes.object, + follow: React.PropTypes.func.isRequired, + unfollow: React.PropTypes.func.isRequired, + onOpenClick: React.PropTypes.func.isRequired, + open: React.PropTypes.bool.isRequired, + isFollowingAuthor: React.PropTypes.bool.isRequired, + highlightFirstItem: React.PropTypes.bool.isRequired, + context: React.PropTypes.shape({ + title: React.PropTypes.string.isRequired, + items: React.PropTypes.array.isRequired, + }).isRequired, +} + +const mapStateToProps = (state) => ({ + // show: state.browser.browserProps.show, + currentTrack: selectors.currentTrack(state), + isFollowingAuthor: selectors.currentUser(state) && !!selectors.currentUser(state).profile.follows.find( + (u) => (u.id === get(state, 'browser.browserProps.show.user.id')) + ), +}) +const mapDispatchToProps = (dispatch) => ({ + playItem: (item) => dispatch(player.jumpToItem(item)), + onAddClick: (item) => dispatch(modal.showModal('ADD_ITEM', { item })), + // onPlayClick: (show, item) => { + // const items = show.items.slice(show.items.indexOf(item)).map(i => ({ + // ...i, + // context: show, + // })) + // dispatch(player.playItems(items)) + // }, + follow: (username) => dispatch(auth.follow(username)), + unfollow: (username) => dispatch(auth.unfollow(username)), + onOpenClick: (context) => dispatch(browser.openContext(context.id)), +}) +export default connect(mapStateToProps, mapDispatchToProps)(Context) diff --git a/app/l8pr/static/components/Context/style.scss b/app/l8pr/static/components/Context/style.scss new file mode 100644 index 0000000..885165c --- /dev/null +++ b/app/l8pr/static/components/Context/style.scss @@ -0,0 +1,37 @@ +.Context { + display: flex; + flex-direction: column; + align-items: stretch; +} +.Context__cover { + color: white; + display: flex; + flex-direction: column; + align-items: stretch; + flex-grow: 1; + background-size: cover; + background-repeat: no-repeat; + background-position: 50% 50%; + .Context__title { + font-family: 'Montserrat'; + padding: 5px 10px; + } + .Context__actions { + align-self: center; + flex-grow: 1; + align-content: center; + } +} +.Context__list { + display: flex; + flex-direction: column; + overflow-y: auto; + max-height: 0vh; + transition: all .25s; +} +.Context--highlightFirstItem { + .Context__cover { background-color: $colorShow; } +} +.Context--opened { + .Context__list { max-height: 20vh } +} diff --git a/app/l8pr/static/components/ContextsList/index.js b/app/l8pr/static/components/ContextsList/index.js new file mode 100644 index 0000000..e8e381e --- /dev/null +++ b/app/l8pr/static/components/ContextsList/index.js @@ -0,0 +1,70 @@ +import React from 'react' +import { ListItem } from '../index' +import { AutoSizer, List } from 'react-virtualized' +import './style.scss' + + +export default class ContextsList extends React.Component { + _rowRenderer({ index, key, style }) { + const { contexts, onItemPlayClick, onPlayShowClick, onAddClick } = this.props + const item = contexts[index] + if (item.type === 'context') { + return ( +
+
+
{item.title}
+
+ {item.size} items + {item.duration} +
+
+ share + more_horiz +
+
+
+ ) + } else { + return ( +
+ +
+ ) + } + } + componentDidUpdate() { + this.refs.AutoSizer.refs.List.recomputeRowHeights() + } + render() { + const { contexts } = this.props + return ( +
+ + {({ width, height }) => ( + + )} + +
+ ) + } +} + +ContextsList.propTypes = { + contexts: React.PropTypes.array.isRequired, + onItemPlayClick: React.PropTypes.func.isRequired, + onPlayShowClick: React.PropTypes.func, + onAddClick: React.PropTypes.func, +} diff --git a/app/l8pr/static/components/ContextsList/style.scss b/app/l8pr/static/components/ContextsList/style.scss new file mode 100644 index 0000000..3e9054f --- /dev/null +++ b/app/l8pr/static/components/ContextsList/style.scss @@ -0,0 +1,36 @@ +.ContextsList { + flex-grow: 1; +} + +.ContextsList__cover { + padding: 20px; + background-color: mediumspringgreen; + display: flex; + +} + +.ContextsList__filter { + padding: 20px; + background-color: purple; + color: white; + font-size: 1.4rem; +} + +.ContextsList__filter .context__actions { + display: flex; + flex-wrap: wrap; +} + +.ContextsList__filter .context__actions div { + margin-right: 25px; +} + +.ContextsList__filter .context__actions i { + margin-left: .5rem; +} +.context__title { + font-weight: 600; +} +.context__details { + +} diff --git a/app/l8pr/static/components/Home/index.js b/app/l8pr/static/components/Home/index.js new file mode 100644 index 0000000..6a19f18 --- /dev/null +++ b/app/l8pr/static/components/Home/index.js @@ -0,0 +1,95 @@ +import React from 'react' +import { connect } from 'react-redux' +import { Screen } from '../index' +import { Strip, ModalsContainer } from '../index' +import { play, pause, next } from '../../actions/player' +import { SOUNDCLOUD_API } from '../../utils/config' +import * as selectors from '../../selectors' +import _IdleMonitor from 'react-simple-idle-monitor' +import './style.scss' + +const IdleMonitor = connect()(_IdleMonitor) + +class HomeView extends React.Component { + + static propTypes = { + media: React.PropTypes.object, + playing: React.PropTypes.bool, + onEnd: React.PropTypes.func, + onPlay: React.PropTypes.func, + onPause: React.PropTypes.func, + volume: React.PropTypes.number, + } + + constructor(props) { + super(props) + this.state = { + progress: 0, + loaded: 0, + } + } + + onSeekTo = (value) => { + this.refs.player.seekTo(value) + } + + render() { + const { + media, + playing, + volume, + onEnd, + onPlay, + onPause, + } = this.props + return ( + +
+ + {this.props.media && + (this.setState({ + progress: p.played, + loaded: p.loaded, + }))} + onReady={()=>(this.setState({ + progress: 0, + loaded: 0, + }))}/> + } + +
+
+ ) + } +} + +const mapStateToProps = (state) => ({ + media: selectors.currentTrack(state), + playing: state.player.playing, + volume: state.player.muted ? 0 : 1, +}) + +const mapDispatchToProps = (dispatch) => ({ + onEnd: () => (dispatch(next())), + onPlay: (args) => (dispatch(play(args))), + onPause: (args) => (dispatch(pause(args))), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(HomeView) +export { HomeView as HomeViewNotConnected } diff --git a/app/l8pr/static/components/Home/style.scss b/app/l8pr/static/components/Home/style.scss new file mode 100644 index 0000000..294bde7 --- /dev/null +++ b/app/l8pr/static/components/Home/style.scss @@ -0,0 +1,22 @@ +.Home { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + font-family: $primaryFont; + overflow: hidden; + font-size: 13px; + display: flex; + flex-direction: column; + justify-content: flex-end; +} + +.material-icons { + font-size: 16px; + vertical-align: middle; +} + +i, a { + cursor: pointer; +} diff --git a/app/l8pr/static/components/ListItem/Preview.js b/app/l8pr/static/components/ListItem/Preview.js new file mode 100644 index 0000000..8ad9ba4 --- /dev/null +++ b/app/l8pr/static/components/ListItem/Preview.js @@ -0,0 +1,14 @@ +import React from 'react' + +function Preview({ image }) { + return ( +
+ playlist_play + play_arrow +
+ ) +} + +Preview.propTypes = { image: React.PropTypes.string.isRequired } + +export default Preview diff --git a/app/l8pr/static/components/ListItem/index.js b/app/l8pr/static/components/ListItem/index.js new file mode 100644 index 0000000..d665d99 --- /dev/null +++ b/app/l8pr/static/components/ListItem/index.js @@ -0,0 +1,117 @@ +import React from 'react' +import './style.scss' +import { getDuration } from '../../utils' +import Preview from './Preview' + +const sourceIcones = { + youtube: 'youtube', + soundcloud: 'soundcloud', + vimeo: 'vimeo', +} + + +function getItemType(item) { + if (item.url) { + return Track + } else if (item.items) { + return Show + } else if (item.username) { + return User + } +} + +export default function ListItem(props) { + const SpecificItem = getItemType(props.item) + const classNames = [ + 'ListItem', + props.isPlaying ? 'ListItem--isPlaying': null, + props.isSelected ? 'ListItem--isSelected': null, + ].join(' ') + return () +} + +function Show({ key, item, className='', onPlayClick, isPlaying, style, onPlayShowClick, onShowClick }) { + return ( +
(onShowClick && onShowClick(item))}> + +
+
{item.title}
+
+ {item.user.username}  + {item.items.length} tracks  + {getDuration(item.items)} +
+ +
+
+ ) +} + +function Track({ key, item, className='', onPlayClick, isPlaying, style, onAddClick, onShowClick, displayShows=false }) { + return ( +
(onPlayClick(item))} style={style}> + {!isPlaying && } +
+
+
{item.title}
+
{getDuration(item)}
+