Автоматический постинг своего Instagram на сайт

Как показала практика, очень удобно размещать фотографии на сайте, импортируя их автоматически из Instagram аккаунта. Сегодня поговорим об этом.

Сначала создадим приложение в нашем Django проекте.

Консоль

(env) ./project $ cd apps/
(env) ./project/apps $ ../manage.py startapp instagram 
(env) ./project/apps $ mv instagram/apps.py instagram/app.py
(env) ./project $ cd ../

Поправим файлы

./apps/instagram/__init__.py

default_app_config = "apps.instagram.app.InstagramConfig"

./apps/instagram/app.py

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.apps import AppConfig


class InstagramConfig(AppConfig):
    name = 'apps.instagram'
    verbose_name = u'Instagram фото'

В файле models.py используется сторонний модуль enum, почитать о котором можно в разделе «Утилиты для облегчения разработки»

./apps/instagram/models.py

# -*- coding:utf-8 -*-
from __future__ import unicode_literals

from sorl.thumbnail import ImageField, get_thumbnail

from django.db import models
from django.conf import settings

from libs import enum

from utils import PathAndRename


class InstagramType(enum.Enum):
    Image = enum.Item('image', u'Фотопост')
    Video = enum.Item('video', u'Видеопост')

class InstagramPost(models.Model):
    instagram_id = models.CharField(u'Instagram ID', max_length=64)
    created_time = models.DateTimeField(u'Дата добавления')
    caption = models.TextField(u'Подпись', null=True, blank=True)
    comments = models.IntegerField(u'Количество коментариев', default=0)
    likes = models.IntegerField(u'Количество лайков', default=0)
    link = models.CharField(u'Ссылка на пост', max_length=255)
    type = models.CharField(u'Тип поста', max_length=12, choices=InstagramType,
                            default=InstagramType.Image, null=True, blank=True)
    carousel_media = models.BooleanField(u'Карусель', default=False)
    import_content = models.BooleanField(u'Контент импортирован', default=False)

    def thumbnail_400(self):
        image = self.images.all().first()
        if image and image.image:
            return image.size_X(400)
        else:
            return None

    def main_photo(self):
        image = self.images.all().first()
        if image and image.image:
            return image.image
        else:
            return None

    class Meta:
        verbose_name = u'пост из инстаграмма'
        verbose_name_plural = u'Пост из инстаграмма'
        ordering = ['-created_time']

    def __unicode__(self):
        return self.instagram_id

class InstagramImage(models.Model):
    path = PathAndRename("instagram/import/")
    instagram = models.ForeignKey('InstagramPost', verbose_name=u'Пост', related_name='images')
    t_link = models.CharField(u'Ссылка на thumbnail фото', max_length=255, null=True, blank=True)
    s_link = models.CharField(u'Ссылка на маленькое фото', max_length=255, null=True, blank=True)
    i_link = models.CharField(u'Ссылка на большое фото', max_length=255, null=True, blank=True)
    image = ImageField(u'Фото', upload_to=path, null=True, blank=True)
    processed = models.BooleanField(u'Фото скачано на локал', default=False )

    def size_150(self):
        return get_thumbnail(self.image, '150x150', crop='center', quality=70)

    def size_X(self, size):
        return get_thumbnail(self.image, '%dx%d' % (size, size), crop='center', quality=70)

    class Meta:
        verbose_name = u'фото инстаграм поста'
        verbose_name_plural = u'Фото инстаграм поста'

    def __unicode__(self):
        return u'%s' % self.id

class InstagramVideo(models.Model):
    instagram = models.ForeignKey('InstagramPost', verbose_name=u'Пост', related_name='videos')
    l_link = models.CharField(u'Ссылка на видео низкого качества', max_length=255, null=True, blank=True)
    h_link = models.CharField(u'Ссылка на видео высокого качества', max_length=255, null=True, blank=True)

    class Meta:
        verbose_name = u'видео инстаграм поста'
        verbose_name_plural = u'Видео инстаграм поста'

    def __unicode__(self):
        return u'%s' % self.id

class InstagramTag(models.Model):
    instagram = models.ForeignKey('InstagramPost', verbose_name=u'Пост', related_name='tags')
    tag = models.CharField(u'Тэг', max_length=255)

    class Meta:
        verbose_name = u'тег инстаграм поста'
        verbose_name_plural = u'Теги инстаграм поста'
        ordering = ['tag']

    def __unicode__(self):
        return u'#%s' % self.tag

./apps/instagram/forms.py

# -*- coding:utf-8 -*-
from django import forms
from django.contrib.admin.widgets import AdminFileWidget
from django.utils.safestring import mark_safe
from sorl.thumbnail import get_thumbnail

from .models import InstagramImage

class AdminImageWidget(AdminFileWidget):

    def render(self, name, value, attrs=None):
        output = []
        if value and getattr(value, "url", None):
            output.append('' % get_thumbnail(value, '100x100', crop='center', quality=70).url)
        output.append(super(AdminFileWidget, self).render(name, value, attrs))
        return mark_safe(u''.join(output))

class ImageForm(forms.ModelForm):

    class Meta:
        model = InstagramImage
        fields = '__all__'
        widgets = {
            'image' : AdminImageWidget,
        }

./apps/instagram/admin.py

# -*- coding:utf-8 -*-
from __future__ import unicode_literals

import nested_admin

from django.contrib import admin

from apps.instagram.models import InstagramImage, InstagramVideo, InstagramTag, InstagramPost
from apps.instagram.forms import ImageForm


class InstagramImageInline(nested_admin.NestedStackedInline):
    form = ImageForm
    model = InstagramImage
    extra = 0
    fieldsets = (
        ( u'', {
            'fields' : (
                ('image', ),
            )
        }),
    )

class InstagramVideoInline(admin.TabularInline):
    model = InstagramVideo
    extra = 0

class InstagramTagInline(admin.TabularInline):
    model = InstagramTag
    extra = 0

class InstagramPostAdmin(admin.ModelAdmin):
    list_display = ('created_time', 'thumbnail_150', 'import_content', 'caption')
    inlines = [
        InstagramImageInline,
        InstagramVideoInline,
        InstagramTagInline,
    ]
admin.site.register(InstagramPost, InstagramPostAdmin)

Добавляем в settings.py в INSTALLED_APPS новый модуль

./project/settings.py

...
INSTALLED_APPS = [
    ...
    'apps.instagram',
    ...
...

Делаем и применяем миграции

Консоль

(env) ./project $ ./manage.py makemigrations
(env) ./project $ ./manage.py migrate

Я предлагаю следующую схему работы: сначала получаем последние посты из Instagram, потом парсим вывод и сохраняем себе на сайт. Есть одно ограничение, скачиваются не больше 20 постов.

Для такой работы нам надо получить получить Instagram access token. Делается это на странице https://www.instagram.com/developer/

Нужно нажать на кнопку Register Your Application.

Далее нужно заполнить форму, перейти на вкладку Security и снять галочку с пункта Disable implicit OAuth и нажать на кнопку Register

Теперь на странице https://www.instagram.com/developer/clients/manage/ должно отобразиться новое приложение.

Получим токен. Нужно в следующей ссылке заменить CLIENT_ID на строку из приложения, и URL на Valid redirect URIs (в моем примере это «https://omoroot.ru/»):

https://instagram.com/oauth/authorize/?client_id=CLIENT_ID&redirect_uri=URL&response_type=token

После перехода по этой ссылке, должна открыться страница, где нужно подтвердить доступ к новому приложению.

После нажатия на кнопку «Авторизовать» будет редирект на сайт, и в строке с URL будет написан ваш access_token. В нем будет две точки. Строка до первой точки — USER_ID, весь токен — ACCESS_TOKEN

Теперь нужно написать команды импорта новых постов и импорта содержимого этих постов. Для этого сначала нужно создать структуру каталогов:

Консоль

(env) ./project $ mkdir -p apps/instagram/management/commands/
(env) ./project $ touch apps/instagram/management/__init__.py
(env) ./project $ touch apps/instagram/management/commands/__init__.py
(env) ./project $ touch apps/instagram/management/commands/import_instagram.py
(env) ./project $ touch apps/instagram/management/commands/import_instagram_content.py

Команда получения новых постов

apps/instagram/management/commands/import_instagram.py

# -*- coding: utf-8 -*-
from django.core.management.base import BaseCommand, CommandError

from requests import get
from datetime import datetime
import pytz

from apps.instagram.models import InstagramType, InstagramPost, InstagramImage, InstagramVideo, InstagramTag

USER_ID = ''
ACCESS_TOKEN  = ''

def get_data_from_json(json_text):
    answer = list()
    for element in json_text['data']:
        answer.append(element)
    return answer

def get_media(user_id, access_token):
    answer = list()
    data = get('https://api.instagram.com/v1/users/%s/media/recent/?access_token=%s' % (user_id, access_token), verify=True).json()
    try:
        if data[u'meta'][u'code'] == 200:
            if data[u'pagination'] == {}:
                answer += get_data_from_json(data)
            else:
                answer += get_data_from_json(data)
                while True:
                    if data[u'pagination'] != {}:
                        data = get(data[u'pagination'][u'next_url'], verify=True).json()
                        answer += get_data_from_json(data)
                    else:
                        break
        else:
            answer = list()
    except:
        answer = list()
    return answer

class Command(BaseCommand):

    def handle(self, *args, **options):
        answer = get_media(USER_ID, ACCESS_TOKEN)
        already_added = InstagramPost.objects.all().values_list('instagram_id', flat=True)
        for item in answer:
            instagram_id = item[u'id']
            # Если нет такого поста, то добавляем
            if instagram_id not in already_added:
                created_time = datetime.fromtimestamp(int(item[u'created_time']), tz=pytz.UTC)
                type = item[u'type']
                if type in [t for (t, d) in InstagramType]:
                    type = InstagramType.by_value(type)
                    carousel_media = False
                else:
                    type = None
                    carousel_media = True
                ip = InstagramPost.objects.create(
                    instagram_id = instagram_id,
                    created_time = created_time,
                    comments = int(item[u'comments'][u'count']),
                    likes = int(item[u'likes'][u'count']),
                    link = item[u'link'],
                    type = item[u'type'],
                    carousel_media = carousel_media
                )
                try:
                    ip.caption = item[u'caption'][u'text']
                    ip.save()
                except:
                    pass
                # Добавляем теги
                try:
                    for tag in item[u'tags']:
                        InstagramTag.objects.create(
                            instagram = ip,
                            tag = tag
                        )
                except:
                    pass
                # Если пост не карусель
                if not ip.carousel_media:
                    ii = InstagramImage.objects.create(
                        instagram = ip,
                        t_link = item[u'images'][u'thumbnail'][u'url'],
                        s_link = item[u'images'][u'low_resolution'][u'url'],
                        i_link = item[u'images'][u'standard_resolution'][u'url']
                    )
                    # Если это видео пост
                    if ip.type == InstagramType.Video:
                        iv = InstagramVideo.objects.create(
                            instagram = ip,
                            l_link = item[u'videos'][u'low_resolution'][u'url'],
                            h_link = item[u'videos'][u'standard_resolution'][u'url']
                        )
                # Пост — карусель
                else:
                    for carousel_item in item[u'carousel_media']:
                        # Если это фото пост
                        if u'images' in carousel_item:
                            ii = InstagramImage.objects.create(
                                instagram = ip,
                                t_link = carousel_item[u'images'][u'thumbnail'][u'url'],
                                s_link = carousel_item[u'images'][u'low_resolution'][u'url'],
                                i_link = carousel_item[u'images'][u'standard_resolution'][u'url']
                            )
                        # Если это видео пост
                        if u'videos' in carousel_item:
                            iv = InstagramVideo.objects.create(
                                instagram = ip,
                                l_link = carousel_item[u'videos'][u'low_resolution'][u'url'],
                                h_link = carousel_item[u'videos'][u'standard_resolution'][u'url']
                            )

Команда сохранения картинок на локале. Дело в том, что импортированные ссылки на картинки со временем протухают, поэтому нужно их сохранить.

apps/instagram/management/commands/import_instagram_content.py

# -*- coding: utf-8 -*-
from django.conf import settings
from django.core import files
from django.core.management.base import BaseCommand, CommandError

import requests
import tempfile

from apps.instagram.models import InstagramType, InstagramPost, InstagramImage, \
                                InstagramVideo, InstagramTag

def get_media(url, obj):
    request = requests.get(url, stream=True)
    if request.status_code != requests.codes.ok:
        return None
    filename = url.split('/')[-1]
    lf = tempfile.NamedTemporaryFile()
    for block in request.iter_content(1024 * 8):
        if not block:
            break
        lf.write(block)
    obj.image.save(filename, files.File(lf))
    return obj


class Command(BaseCommand):

    def handle(self, *args, **options):
        posts_for_import = InstagramPost.objects.filter(import_content=False)
        for post in posts_for_import:
            for image in post.images.all():
                if not image.processed:
                    image.processed = True
                    image.save()
                    get_media(image.i_link, image)
            post.import_content = True
            post.save()

Дальше остается только прописать обе команды в крон так, чтобы они выполнялись одна за другой.

Консоль

(env) ./project $ ./manage.py import_instagram
(env) ./project $ ./manage.py import_instagram_content