Skip to content

Commit 0329ce8

Browse files
authored
Add audio uploads (mastodon#11123)
* Add audio uploads Fix mastodon#4827 Accept uploads of OGG, WAV, FLAC, OPUS and MP3 files, and converts them to OGG. Media attachments get a new `audio` type. In the UI, audio uploads are displayed identically to video uploads. * Improve code style
1 parent c2203d4 commit 0329ce8

11 files changed

Lines changed: 76 additions & 44 deletions

File tree

app/controllers/media_controller.rb

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ class MediaController < ApplicationController
77

88
before_action :set_media_attachment
99
before_action :verify_permitted_status!
10+
before_action :check_playable, only: :player
11+
before_action :allow_iframing, only: :player
1012

1113
content_security_policy only: :player do |p|
1214
p.frame_ancestors(false)
@@ -18,8 +20,6 @@ def show
1820

1921
def player
2022
@body_classes = 'player'
21-
response.headers['X-Frame-Options'] = 'ALLOWALL'
22-
raise ActiveRecord::RecordNotFound unless @media_attachment.video? || @media_attachment.gifv?
2323
end
2424

2525
private
@@ -34,4 +34,12 @@ def verify_permitted_status!
3434
# Reraise in order to get a 404 instead of a 403 error code
3535
raise ActiveRecord::RecordNotFound
3636
end
37+
38+
def check_playable
39+
not_found unless @media_attachment.larger_media_format?
40+
end
41+
42+
def allow_iframing
43+
response.headers['X-Frame-Options'] = 'ALLOWALL'
44+
end
3745
end

app/javascript/mastodon/components/status.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -333,17 +333,17 @@ class Status extends ImmutablePureComponent {
333333
media={status.get('media_attachments')}
334334
/>
335335
);
336-
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
337-
const video = status.getIn(['media_attachments', 0]);
336+
} else if (['video', 'audio'].includes(status.getIn(['media_attachments', 0, 'type']))) {
337+
const attachment = status.getIn(['media_attachments', 0]);
338338

339339
media = (
340340
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
341341
{Component => (
342342
<Component
343-
preview={video.get('preview_url')}
344-
blurhash={video.get('blurhash')}
345-
src={video.get('url')}
346-
alt={video.get('description')}
343+
preview={attachment.get('preview_url')}
344+
blurhash={attachment.get('blurhash')}
345+
src={attachment.get('url')}
346+
alt={attachment.get('description')}
347347
width={this.props.cachedMediaWidth}
348348
height={110}
349349
inline

app/javascript/mastodon/features/compose/components/upload_button.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
77
import ImmutablePropTypes from 'react-immutable-proptypes';
88

99
const messages = defineMessages({
10-
upload: { id: 'upload_button.label', defaultMessage: 'Add media (JPEG, PNG, GIF, WebM, MP4, MOV)' },
10+
upload: { id: 'upload_button.label', defaultMessage: 'Add media ({formats})' },
1111
});
1212

13+
const SUPPORTED_FORMATS = 'JPEG, PNG, GIF, WebM, MP4, MOV, OGG, WAV, MP3, FLAC';
14+
1315
const makeMapStateToProps = () => {
1416
const mapStateToProps = state => ({
1517
acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']),
@@ -60,9 +62,9 @@ class UploadButton extends ImmutablePureComponent {
6062

6163
return (
6264
<div className='compose-form__upload-button'>
63-
<IconButton icon='camera' title={intl.formatMessage(messages.upload)} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} />
65+
<IconButton icon='camera' title={intl.formatMessage(messages.upload, { formats: SUPPORTED_FORMATS })} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} />
6466
<label>
65-
<span style={{ display: 'none' }}>{intl.formatMessage(messages.upload)}</span>
67+
<span style={{ display: 'none' }}>{intl.formatMessage(messages.upload, { formats: SUPPORTED_FORMATS })}</span>
6668
<input
6769
key={resetFileKey}
6870
ref={this.setRef}

app/javascript/mastodon/features/compose/containers/upload_button_container.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import UploadButton from '../components/upload_button';
33
import { uploadCompose } from '../../../actions/compose';
44

55
const mapStateToProps = state => ({
6-
disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => m.get('type') === 'video')),
6+
disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => ['video', 'audio'].includes(m.get('type')))),
77
unavailable: state.getIn(['compose', 'poll']) !== null,
88
resetFileKey: state.getIn(['compose', 'resetFileKey']),
99
});

app/javascript/mastodon/features/status/components/detailed_status.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -107,15 +107,15 @@ export default class DetailedStatus extends ImmutablePureComponent {
107107
}
108108

109109
if (status.get('media_attachments').size > 0) {
110-
if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
111-
const video = status.getIn(['media_attachments', 0]);
110+
if (['video', 'audio'].includes(status.getIn(['media_attachments', 0, 'type']))) {
111+
const attachment = status.getIn(['media_attachments', 0]);
112112

113113
media = (
114114
<Video
115-
preview={video.get('preview_url')}
116-
blurhash={video.get('blurhash')}
117-
src={video.get('url')}
118-
alt={video.get('description')}
115+
preview={attachment.get('preview_url')}
116+
blurhash={attachment.get('blurhash')}
117+
src={attachment.get('url')}
118+
alt={attachment.get('description')}
119119
width={300}
120120
height={150}
121121
inline

app/javascript/mastodon/locales/en.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@
369369
"trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
370370
"ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
371371
"upload_area.title": "Drag & drop to upload",
372-
"upload_button.label": "Add media (JPEG, PNG, GIF, WebM, MP4, MOV)",
372+
"upload_button.label": "Add media ({formats})",
373373
"upload_error.limit": "File upload limit exceeded.",
374374
"upload_error.poll": "File upload not allowed with polls.",
375375
"upload_form.description": "Describe for the visually impaired",

app/models/media_attachment.rb

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,16 @@
2424
class MediaAttachment < ApplicationRecord
2525
self.inheritance_column = nil
2626

27-
enum type: [:image, :gifv, :video, :unknown]
27+
enum type: [:image, :gifv, :video, :unknown, :audio]
2828

2929
IMAGE_FILE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp'].freeze
3030
VIDEO_FILE_EXTENSIONS = ['.webm', '.mp4', '.m4v', '.mov'].freeze
31+
AUDIO_FILE_EXTENSIONS = ['.ogg', '.oga', '.mp3', '.wav', '.flac', '.opus'].freeze
3132

3233
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze
3334
VIDEO_MIME_TYPES = ['video/webm', 'video/mp4', 'video/quicktime'].freeze
3435
VIDEO_CONVERTIBLE_MIME_TYPES = ['video/webm', 'video/quicktime'].freeze
36+
AUDIO_MIME_TYPES = ['audio/wave', 'audio/wav', 'audio/x-wav', 'audio/x-pn-wave', 'audio/ogg', 'audio/mpeg', 'audio/webm', 'audio/flac'].freeze
3537

3638
BLURHASH_OPTIONS = {
3739
x_comp: 4,
@@ -65,6 +67,13 @@ class MediaAttachment < ApplicationRecord
6567
},
6668
}.freeze
6769

70+
AUDIO_STYLES = {
71+
original: {
72+
format: 'ogg',
73+
convert_options: {},
74+
},
75+
}.freeze
76+
6877
VIDEO_FORMAT = {
6978
format: 'mp4',
7079
convert_options: {
@@ -83,6 +92,11 @@ class MediaAttachment < ApplicationRecord
8392
},
8493
}.freeze
8594

95+
VIDEO_CONVERTED_STYLES = {
96+
small: VIDEO_STYLES[:small],
97+
original: VIDEO_FORMAT,
98+
}.freeze
99+
86100
IMAGE_LIMIT = 8.megabytes
87101
VIDEO_LIMIT = 40.megabytes
88102

@@ -95,9 +109,9 @@ class MediaAttachment < ApplicationRecord
95109
processors: ->(f) { file_processors f },
96110
convert_options: { all: '-quality 90 -strip' }
97111

98-
validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES
99-
validates_attachment_size :file, less_than: IMAGE_LIMIT, unless: :video_or_gifv?
100-
validates_attachment_size :file, less_than: VIDEO_LIMIT, if: :video_or_gifv?
112+
validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES + AUDIO_MIME_TYPES
113+
validates_attachment_size :file, less_than: IMAGE_LIMIT, unless: :larger_media_format?
114+
validates_attachment_size :file, less_than: VIDEO_LIMIT, if: :larger_media_format?
101115
remotable_attachment :file, VIDEO_LIMIT
102116

103117
include Attachmentable
@@ -120,8 +134,12 @@ def needs_redownload?
120134
file.blank? && remote_url.present?
121135
end
122136

123-
def video_or_gifv?
124-
video? || gifv?
137+
def larger_media_format?
138+
video? || gifv? || audio?
139+
end
140+
141+
def audio_or_video?
142+
audio? || video?
125143
end
126144

127145
def to_param
@@ -156,28 +174,24 @@ class << self
156174
private
157175

158176
def file_styles(f)
159-
if f.instance.file_content_type == 'image/gif'
160-
{
161-
small: IMAGE_STYLES[:small],
162-
original: VIDEO_FORMAT,
163-
}
164-
elsif IMAGE_MIME_TYPES.include? f.instance.file_content_type
177+
if f.instance.file_content_type == 'image/gif' || VIDEO_CONVERTIBLE_MIME_TYPES.include?(f.instance.file_content_type)
178+
VIDEO_CONVERTED_STYLES
179+
elsif IMAGE_MIME_TYPES.include?(f.instance.file_content_type)
165180
IMAGE_STYLES
166-
elsif VIDEO_CONVERTIBLE_MIME_TYPES.include?(f.instance.file_content_type)
167-
{
168-
small: VIDEO_STYLES[:small],
169-
original: VIDEO_FORMAT,
170-
}
171-
else
181+
elsif VIDEO_MIME_TYPES.include?(f.instance.file_content_type)
172182
VIDEO_STYLES
183+
else
184+
AUDIO_STYLES
173185
end
174186
end
175187

176188
def file_processors(f)
177189
if f.file_content_type == 'image/gif'
178190
[:gif_transcoder, :blurhash_transcoder]
179-
elsif VIDEO_MIME_TYPES.include? f.file_content_type
191+
elsif VIDEO_MIME_TYPES.include?(f.file_content_type)
180192
[:video_transcoder, :blurhash_transcoder]
193+
elsif AUDIO_MIME_TYPES.include?(f.file_content_type)
194+
[:transcoder]
181195
else
182196
[:lazy_thumbnail, :blurhash_transcoder]
183197
end
@@ -202,7 +216,15 @@ def prepare_description
202216
end
203217

204218
def set_type_and_extension
205-
self.type = VIDEO_MIME_TYPES.include?(file_content_type) ? :video : :image
219+
self.type = begin
220+
if VIDEO_MIME_TYPES.include?(file_content_type)
221+
:video
222+
elsif AUDIO_MIME_TYPES.include?(file_content_type)
223+
:audio
224+
else
225+
:image
226+
end
227+
end
206228
end
207229

208230
def set_meta
@@ -245,7 +267,7 @@ def video_metadata(file)
245267
frame_rate: movie.frame_rate,
246268
duration: movie.duration,
247269
bitrate: movie.bitrate,
248-
}
270+
}.compact
249271
end
250272

251273
def reset_parent_cache

app/serializers/initial_state_serializer.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def accounts
6060
end
6161

6262
def media_attachments
63-
{ accept_content_types: MediaAttachment::IMAGE_FILE_EXTENSIONS + MediaAttachment::VIDEO_FILE_EXTENSIONS + MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES }
63+
{ accept_content_types: MediaAttachment::IMAGE_FILE_EXTENSIONS + MediaAttachment::VIDEO_FILE_EXTENSIONS + MediaAttachment::AUDIO_FILE_EXTENSIONS + MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES + MediaAttachment::AUDIO_MIME_TYPES }
6464
end
6565

6666
private

app/services/post_status_service.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ def validate_media!
100100

101101
@media = @account.media_attachments.where(status_id: nil).where(id: @options[:media_ids].take(4).map(&:to_i))
102102

103-
raise Mastodon::ValidationError, I18n.t('media_attachments.validations.images_and_video') if @media.size > 1 && @media.find(&:video?)
103+
raise Mastodon::ValidationError, I18n.t('media_attachments.validations.images_and_video') if @media.size > 1 && @media.find(&:audio_or_video?)
104104
end
105105

106106
def language_from_option(str)

app/views/stream_entries/_detailed_status.html.haml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
= render partial: 'stream_entries/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: autoplay }
2828

2929
- if !status.media_attachments.empty?
30-
- if status.media_attachments.first.video?
30+
- if status.media_attachments.first.audio_or_video?
3131
- video = status.media_attachments.first
3232
= react_component :video, src: video.file.url(:original), preview: video.file.url(:small), blurhash: video.blurhash, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 670, height: 380, detailed: true, inline: true, alt: video.description do
3333
= render partial: 'stream_entries/attachment_list', locals: { attachments: status.media_attachments }

0 commit comments

Comments
 (0)