Fix #431 - convert gif to webm during upload. Web UI treats them like it did

before. In the API, attachments now can be either image, video or gifv. Gifv
is to be treated like images in terms of behaviour, but are videos by file
type.
This commit is contained in:
Eugen Rochko 2017-03-04 22:17:10 +01:00
parent 4cbeb9a7eb
commit caf5b8e975
17 changed files with 325 additions and 137 deletions

View file

@ -75,11 +75,16 @@ export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL';
export function fetchAccount(id) { export function fetchAccount(id) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch(fetchRelationships([id]));
if (getState().getIn(['accounts', id], null) !== null) {
return;
}
dispatch(fetchAccountRequest(id)); dispatch(fetchAccountRequest(id));
api(getState).get(`/api/v1/accounts/${id}`).then(response => { api(getState).get(`/api/v1/accounts/${id}`).then(response => {
dispatch(fetchAccountSuccess(response.data)); dispatch(fetchAccountSuccess(response.data));
dispatch(fetchRelationships([id]));
}).catch(error => { }).catch(error => {
dispatch(fetchAccountFail(id, error)); dispatch(fetchAccountFail(id, error));
}); });

View file

@ -0,0 +1,21 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
const ExtendedVideoPlayer = React.createClass({
propTypes: {
src: React.PropTypes.string.isRequired
},
mixins: [PureRenderMixin],
render () {
return (
<div>
<video src={this.props.src} autoPlay muted loop />
</div>
);
},
});
export default ExtendedVideoPlayer;

View file

@ -43,6 +43,141 @@ const spoilerButtonStyle = {
zIndex: '100' zIndex: '100'
}; };
const itemStyle = {
boxSizing: 'border-box',
position: 'relative',
float: 'left',
border: 'none',
display: 'block'
};
const thumbStyle = {
display: 'block',
width: '100%',
height: '100%',
textDecoration: 'none',
backgroundSize: 'cover',
cursor: 'zoom-in'
};
const gifvThumbStyle = {
position: 'relative',
zIndex: '1',
width: '100%',
height: '100%',
objectFit: 'cover',
top: '50%',
transform: 'translateY(-50%)',
cursor: 'zoom-in'
};
const Item = React.createClass({
propTypes: {
attachment: ImmutablePropTypes.map.isRequired,
index: React.PropTypes.number.isRequired,
size: React.PropTypes.number.isRequired,
onClick: React.PropTypes.func.isRequired
},
mixins: [PureRenderMixin],
handleClick (e) {
const { index, onClick } = this.props;
if (e.button === 0) {
e.preventDefault();
onClick(index);
}
e.stopPropagation();
},
render () {
const { attachment, index, size } = this.props;
let width = 50;
let height = 100;
let top = 'auto';
let left = 'auto';
let bottom = 'auto';
let right = 'auto';
if (size === 1) {
width = 100;
}
if (size === 4 || (size === 3 && index > 0)) {
height = 50;
}
if (size === 2) {
if (index === 0) {
right = '2px';
} else {
left = '2px';
}
} else if (size === 3) {
if (index === 0) {
right = '2px';
} else if (index > 0) {
left = '2px';
}
if (index === 1) {
bottom = '2px';
} else if (index > 1) {
top = '2px';
}
} else if (size === 4) {
if (index === 0 || index === 2) {
right = '2px';
}
if (index === 1 || index === 3) {
left = '2px';
}
if (index < 2) {
bottom = '2px';
} else {
top = '2px';
}
}
let thumbnail = '';
if (attachment.get('type') === 'image') {
thumbnail = (
<a
href={attachment.get('remote_url') ? attachment.get('remote_url') : attachment.get('url')}
onClick={this.handleClick}
target='_blank'
style={{ background: `url(${attachment.get('preview_url')}) no-repeat center`, ...thumbStyle }}
/>
);
} else if (attachment.get('type') === 'gifv') {
thumbnail = (
<video
src={attachment.get('url')}
onClick={this.handleClick}
autoPlay={true}
loop={true}
muted={true}
style={gifvThumbStyle}
/>
);
}
return (
<div key={attachment.get('id')} style={{ ...itemStyle, left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
{thumbnail}
</div>
);
}
});
const MediaGallery = React.createClass({ const MediaGallery = React.createClass({
getInitialState () { getInitialState () {
@ -61,17 +196,12 @@ const MediaGallery = React.createClass({
mixins: [PureRenderMixin], mixins: [PureRenderMixin],
handleClick (index, e) { handleOpen (e) {
if (e.button === 0) { this.setState({ visible: !this.state.visible });
e.preventDefault();
this.props.onOpenMedia(this.props.media, index);
}
e.stopPropagation();
}, },
handleOpen () { handleClick (index) {
this.setState({ visible: !this.state.visible }); this.props.onOpenMedia(this.props.media, index);
}, },
render () { render () {
@ -80,87 +210,31 @@ const MediaGallery = React.createClass({
let children; let children;
if (!this.state.visible) { if (!this.state.visible) {
let warning;
if (sensitive) { if (sensitive) {
children = ( warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />;
<div style={spoilerStyle} className='media-spoiler' onClick={this.handleOpen}>
<span style={spoilerSpanStyle}><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
</div>
);
} else { } else {
children = ( warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />;
<div style={spoilerStyle} className='media-spoiler' onClick={this.handleOpen}>
<span style={spoilerSpanStyle}><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
</div>
);
} }
children = (
<div style={spoilerStyle} className='media-spoiler' onClick={this.handleOpen}>
<span style={spoilerSpanStyle}>{warning}</span>
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
</div>
);
} else { } else {
const size = media.take(4).size; const size = media.take(4).size;
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} />);
children = media.take(4).map((attachment, i) => {
let width = 50;
let height = 100;
let top = 'auto';
let left = 'auto';
let bottom = 'auto';
let right = 'auto';
if (size === 1) {
width = 100;
}
if (size === 4 || (size === 3 && i > 0)) {
height = 50;
}
if (size === 2) {
if (i === 0) {
right = '2px';
} else {
left = '2px';
}
} else if (size === 3) {
if (i === 0) {
right = '2px';
} else if (i > 0) {
left = '2px';
}
if (i === 1) {
bottom = '2px';
} else if (i > 1) {
top = '2px';
}
} else if (size === 4) {
if (i === 0 || i === 2) {
right = '2px';
}
if (i === 1 || i === 3) {
left = '2px';
}
if (i < 2) {
bottom = '2px';
} else {
top = '2px';
}
}
return (
<div key={attachment.get('id')} style={{ boxSizing: 'border-box', position: 'relative', left: left, top: top, right: right, bottom: bottom, float: 'left', border: 'none', display: 'block', width: `${width}%`, height: `${height}%` }}>
<a href={attachment.get('remote_url') ? attachment.get('remote_url') : attachment.get('url')} onClick={this.handleClick.bind(this, i)} target='_blank' style={{ display: 'block', width: '100%', height: '100%', background: `url(${attachment.get('preview_url')}) no-repeat center`, textDecoration: 'none', backgroundSize: 'cover', cursor: 'zoom-in' }} />
</div>
);
});
} }
return ( return (
<div style={{ ...outerStyle, height: `${this.props.height}px` }}> <div style={{ ...outerStyle, height: `${this.props.height}px` }}>
<div style={spoilerButtonStyle} > <div style={spoilerButtonStyle}>
<IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} onClick={this.handleOpen} /> <IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} onClick={this.handleOpen} />
</div> </div>
{children} {children}
</div> </div>
); );

View file

@ -74,8 +74,8 @@ const Status = React.createClass({
} }
if (status.get('media_attachments').size > 0 && !this.props.muted) { if (status.get('media_attachments').size > 0 && !this.props.muted) {
if (status.getIn(['media_attachments', 0, 'type']) === 'video') { if (status.getIn(['media_attachments', 0, 'type']) === 'video' || (status.get('media_attachments').size === 1 && status.getIn(['media_attachments', 0, 'type']) === 'gifv')) {
media = <VideoPlayer media={status.getIn(['media_attachments', 0])} sensitive={status.get('sensitive')} />; media = <VideoPlayer media={status.getIn(['media_attachments', 0])} autoplay={status.getIn(['media_attachments', 0, 'type']) === 'gifv'} sensitive={status.get('sensitive')} />;
} else { } else {
media = <MediaGallery media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} />; media = <MediaGallery media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} />;
} }

View file

@ -175,7 +175,7 @@ const VideoPlayer = React.createClass({
); );
} else { } else {
return ( return (
<div style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleOpen}> <div style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
{spoilerButton} {spoilerButton}
<span style={spoilerSpanStyle}><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span> <span style={spoilerSpanStyle}><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> <span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
@ -197,7 +197,7 @@ const VideoPlayer = React.createClass({
<div style={{ cursor: 'default', marginTop: '8px', overflow: 'hidden', width: `${width}px`, height: `${height}px`, boxSizing: 'border-box', background: '#000', position: 'relative' }}> <div style={{ cursor: 'default', marginTop: '8px', overflow: 'hidden', width: `${width}px`, height: `${height}px`, boxSizing: 'border-box', background: '#000', position: 'relative' }}>
{spoilerButton} {spoilerButton}
{muteButton} {muteButton}
<video ref={this.setRef} src={media.get('url')} autoPlay='true' loop={true} muted={this.state.muted} style={videoStyle} onClick={this.handleVideoClick} /> <video ref={this.setRef} src={media.get('url')} autoPlay={true} loop={true} muted={this.state.muted} style={videoStyle} onClick={this.handleVideoClick} />
</div> </div>
); );
} }

View file

@ -38,7 +38,7 @@ const DetailedStatus = React.createClass({
let applicationLink = ''; let applicationLink = '';
if (status.get('media_attachments').size > 0) { if (status.get('media_attachments').size > 0) {
if (status.getIn(['media_attachments', 0, 'type']) === 'video') { if (status.getIn(['media_attachments', 0, 'type']) === 'video' || (status.get('media_attachments').size === 1 && status.getIn(['media_attachments', 0, 'type']) === 'gifv')) {
media = <VideoPlayer sensitive={status.get('sensitive')} media={status.getIn(['media_attachments', 0])} width={300} height={150} autoplay />; media = <VideoPlayer sensitive={status.get('sensitive')} media={status.getIn(['media_attachments', 0])} width={300} height={150} autoplay />;
} else { } else {
media = <MediaGallery sensitive={status.get('sensitive')} media={status.get('media_attachments')} height={300} onOpenMedia={this.props.onOpenMedia} />; media = <MediaGallery sensitive={status.get('sensitive')} media={status.get('media_attachments')} height={300} onOpenMedia={this.props.onOpenMedia} />;

View file

@ -9,6 +9,7 @@ import ImageLoader from 'react-imageloader';
import LoadingIndicator from '../../../components/loading_indicator'; import LoadingIndicator from '../../../components/loading_indicator';
import PureRenderMixin from 'react-addons-pure-render-mixin'; import PureRenderMixin from 'react-addons-pure-render-mixin';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ExtendedVideoPlayer from '../../../components/extended_video_player';
const mapStateToProps = state => ({ const mapStateToProps = state => ({
media: state.getIn(['modal', 'media']), media: state.getIn(['modal', 'media']),
@ -131,27 +132,34 @@ const Modal = React.createClass({
return null; return null;
} }
const url = media.get(index).get('url'); const attachment = media.get(index);
const url = attachment.get('url');
let leftNav, rightNav; let leftNav, rightNav, content;
leftNav = rightNav = ''; leftNav = rightNav = content = '';
if (media.size > 1) { if (media.size > 1) {
leftNav = <div style={leftNavStyle} className='modal-container--nav' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>; leftNav = <div style={leftNavStyle} className='modal-container--nav' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>;
rightNav = <div style={rightNavStyle} className='modal-container--nav' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>; rightNav = <div style={rightNavStyle} className='modal-container--nav' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>;
} }
return ( if (attachment.get('type') === 'image') {
<Lightbox {...other}> content = (
{leftNav}
<ImageLoader <ImageLoader
src={url} src={url}
preloader={preloader} preloader={preloader}
imgProps={{ style: imageStyle }} imgProps={{ style: imageStyle }}
/> />
);
} else if (attachment.get('type') === 'gifv') {
content = <ExtendedVideoPlayer src={url} />;
}
return (
<Lightbox {...other}>
{leftNav}
{content}
{rightNav} {rightNav}
</Lightbox> </Lightbox>
); );

View file

@ -104,8 +104,12 @@
overflow: hidden; overflow: hidden;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
height: 110px; position: relative;
display: flex;
.status__attachments__inner {
display: flex;
height: 214px;
}
} }
} }
@ -184,8 +188,12 @@
overflow: hidden; overflow: hidden;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
height: 300px; position: relative;
display: flex;
.status__attachments__inner {
display: flex;
height: 360px;
}
} }
.video-player { .video-player {
@ -231,11 +239,19 @@
text-decoration: none; text-decoration: none;
cursor: zoom-in; cursor: zoom-in;
} }
video {
position: relative;
z-index: 1;
width: 100%;
height: 100%;
object-fit: cover;
top: 50%;
transform: translateY(-50%);
}
} }
.video-item { .video-item {
max-width: 196px;
a { a {
cursor: pointer; cursor: pointer;
} }
@ -258,6 +274,9 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
cursor: pointer; cursor: pointer;
position: absolute;
top: 0;
left: 0;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;

View file

@ -1,15 +1,32 @@
# frozen_string_literal: true # frozen_string_literal: true
class MediaAttachment < ApplicationRecord class MediaAttachment < ApplicationRecord
self.inheritance_column = nil
enum type: [:image, :gifv, :video]
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze
VIDEO_MIME_TYPES = ['video/webm', 'video/mp4'].freeze VIDEO_MIME_TYPES = ['video/webm', 'video/mp4'].freeze
IMAGE_STYLES = { original: '1280x1280>', small: '400x400>' }.freeze
VIDEO_STYLES = {
small: {
convert_options: {
output: {
vf: 'scale=\'min(400\, iw):min(400\, ih)\':force_original_aspect_ratio=decrease',
},
},
format: 'png',
time: 0,
},
}.freeze
belongs_to :account, inverse_of: :media_attachments belongs_to :account, inverse_of: :media_attachments
belongs_to :status, inverse_of: :media_attachments belongs_to :status, inverse_of: :media_attachments
has_attached_file :file, has_attached_file :file,
styles: -> (f) { file_styles f }, styles: ->(f) { file_styles f },
processors: -> (f) { f.video? ? [:transcoder] : [:thumbnail] }, processors: ->(f) { file_processors f },
convert_options: { all: '-quality 90 -strip' } convert_options: { all: '-quality 90 -strip' }
validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES
validates_attachment_size :file, less_than: 8.megabytes validates_attachment_size :file, less_than: 8.megabytes
@ -27,45 +44,45 @@ class MediaAttachment < ApplicationRecord
self.file = URI.parse(url) self.file = URI.parse(url)
end end
def image?
IMAGE_MIME_TYPES.include? file_content_type
end
def video?
VIDEO_MIME_TYPES.include? file_content_type
end
def type
image? ? 'image' : 'video'
end
def to_param def to_param
shortcode shortcode
end end
before_create :set_shortcode before_create :set_shortcode
before_post_process :set_type
class << self class << self
private private
def file_styles(f) def file_styles(f)
if f.instance.image? if f.instance.file_content_type == 'image/gif'
{ {
original: '1280x1280>', small: IMAGE_STYLES[:small],
small: '400x400>', original: {
} format: 'webm',
else
{
small: {
convert_options: { convert_options: {
output: { output: {
vf: 'scale=\'min(400\, iw):min(400\, ih)\':force_original_aspect_ratio=decrease', 'c:v' => 'libvpx',
'crf' => 6,
'b:v' => '500K',
}, },
}, },
format: 'png',
time: 1,
}, },
} }
elsif IMAGE_MIME_TYPES.include? f.instance.file_content_type
IMAGE_STYLES
else
VIDEO_STYLES
end
end
def file_processors(f)
if f.file_content_type == 'image/gif'
[:gif_transcoder]
elsif VIDEO_MIME_TYPES.include? f.file_content_type
[:transcoder]
else
[:thumbnail]
end end
end end
end end
@ -80,4 +97,8 @@ class MediaAttachment < ApplicationRecord
break if MediaAttachment.find_by(shortcode: shortcode).nil? break if MediaAttachment.find_by(shortcode: shortcode).nil?
end end
end end
def set_type
self.type = VIDEO_MIME_TYPES.include?(file_content_type) ? :video : :image
end
end end

View file

@ -1,5 +1,5 @@
object @media object @media
attribute :id, :type attribute :id, :type
node(:url) { |media| full_asset_url(media.file.url( :original)) } node(:url) { |media| full_asset_url(media.file.url(:original)) }
node(:preview_url) { |media| full_asset_url(media.file.url( :small)) } node(:preview_url) { |media| full_asset_url(media.file.url(:small)) }
node(:text_url) { |media| medium_url(media) } node(:text_url) { |media| medium_url(media) }

View file

@ -22,9 +22,9 @@
.detailed-status__attachments .detailed-status__attachments
- if status.sensitive? - if status.sensitive?
= render partial: 'stream_entries/content_spoiler' = render partial: 'stream_entries/content_spoiler'
- status.media_attachments.each do |media| .status__attachments__inner
.media-item - status.media_attachments.each do |media|
= link_to '', (media.remote_url.blank? ? media.file.url(:original) : media.remote_url), style: "background-image: url(#{media.file.url(:original)})", target: '_blank', rel: 'noopener', class: "u-#{media.video? ? 'video' : 'photo'}" = render partial: 'stream_entries/media', locals: { media: media }
%div.detailed-status__meta %div.detailed-status__meta
%data.dt-published{ value: status.created_at.to_time.iso8601 } %data.dt-published{ value: status.created_at.to_time.iso8601 }

View file

@ -0,0 +1,4 @@
.media-item
= link_to media.remote_url.blank? ? media.file.url(:original) : media.remote_url, style: media.image? ? "background-image: url(#{media.file.url(:original)})" : "", target: '_blank', rel: 'noopener', class: "u-#{media.video? || media.gifv? ? 'video' : 'photo'}" do
- unless media.image?
%video{ src: media.file.url(:original), autoplay: true, loop: true }/

View file

@ -22,11 +22,12 @@
- if status.sensitive? - if status.sensitive?
= render partial: 'stream_entries/content_spoiler' = render partial: 'stream_entries/content_spoiler'
- if status.media_attachments.first.video? - if status.media_attachments.first.video?
.video-item .status__attachments__inner
= link_to (status.media_attachments.first.remote_url.blank? ? status.media_attachments.first.file.url(:original) : status.media_attachments.first.remote_url), style: "background-image: url(#{status.media_attachments.first.file.url(:small)})", target: '_blank', rel: 'noopener', class: 'u-video' do .video-item
.video-item__play = link_to (status.media_attachments.first.remote_url.blank? ? status.media_attachments.first.file.url(:original) : status.media_attachments.first.remote_url), style: "background-image: url(#{status.media_attachments.first.file.url(:small)})", target: '_blank', rel: 'noopener', class: 'u-video' do
= fa_icon('play') .video-item__play
= fa_icon('play')
- else - else
- status.media_attachments.each do |media| .status__attachments__inner
.media-item - status.media_attachments.each do |media|
= link_to '', (media.remote_url.blank? ? media.file.url(:original) : media.remote_url), style: "background-image: url(#{media.file.url(:original)})", target: '_blank', rel: 'noopener', class: "u-#{media.video? ? 'video' : 'photo'}" = render partial: 'stream_entries/media', locals: { media: media }

View file

@ -2,12 +2,13 @@ require_relative 'boot'
require 'rails/all' require 'rails/all'
require_relative '../app/lib/exceptions'
# Require the gems listed in Gemfile, including any gems # Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production. # you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups) Bundler.require(*Rails.groups)
require_relative '../app/lib/exceptions'
require_relative '../lib/paperclip/gif_transcoder'
Dotenv::Railtie.load Dotenv::Railtie.load
module Mastodon module Mastodon

View file

@ -0,0 +1,12 @@
class AddTypeToMediaAttachments < ActiveRecord::Migration[5.0]
def up
add_column :media_attachments, :type, :integer, default: 0, null: false
MediaAttachment.where(file_content_type: MediaAttachment::IMAGE_MIME_TYPES).update_all(type: MediaAttachment.types[:image])
MediaAttachment.where(file_content_type: MediaAttachment::VIDEO_MIME_TYPES).update_all(type: MediaAttachment.types[:video])
end
def down
remove_column :media_attachments, :type
end
end

View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170303212857) do ActiveRecord::Schema.define(version: 20170304202101) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -98,6 +98,7 @@ ActiveRecord::Schema.define(version: 20170303212857) do
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.string "shortcode" t.string "shortcode"
t.integer "type", default: 0, null: false
t.index ["shortcode"], name: "index_media_attachments_on_shortcode", unique: true, using: :btree t.index ["shortcode"], name: "index_media_attachments_on_shortcode", unique: true, using: :btree
t.index ["status_id"], name: "index_media_attachments_on_status_id", using: :btree t.index ["status_id"], name: "index_media_attachments_on_status_id", using: :btree
end end

View file

@ -0,0 +1,21 @@
# frozen_string_literal: true
module Paperclip
# This transcoder is only to be used for the MediaAttachment model
# to convert animated gifs to webm
class GifTranscoder < Paperclip::Processor
def make
num_frames = identify('-format %n :file', file: file.path).to_i
return file unless options[:style] == :original && num_frames > 1
final_file = Paperclip::Transcoder.make(file, options, attachment)
attachment.instance.file_file_name = 'media.webm'
attachment.instance.file_content_type = 'video/webm'
attachment.instance.type = MediaAttachment.types[:gifv]
final_file
end
end
end