Skip to content

Commit 284fc2c

Browse files
authored
Merge pull request #234 from ShaneIsrael/develop
Improved video scanning and uploading
2 parents b2e386f + 16ed9ea commit 284fc2c

File tree

8 files changed

+139
-34
lines changed

8 files changed

+139
-34
lines changed

app/client/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "fireshare",
3-
"version": "1.2.15",
3+
"version": "1.2.16",
44
"private": true,
55
"dependencies": {
66
"@emotion/react": "^11.9.0",

app/client/src/components/admin/UploadCard.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const Input = styled('input')({
1111

1212
const numberFormat = new Intl.NumberFormat('en-US')
1313

14-
const UploadCard = ({ authenticated, feedView = false, publicUpload = false, cardWidth, handleAlert }) => {
14+
const UploadCard = ({ authenticated, feedView = false, publicUpload = false, fetchVideos, cardWidth, handleAlert }) => {
1515
const cardHeight = cardWidth / 1.77 + 32
1616
const [selectedFile, setSelectedFile] = React.useState()
1717
const [isSelected, setIsSelected] = React.useState(false)
@@ -58,7 +58,13 @@ const UploadCard = ({ authenticated, feedView = false, publicUpload = false, car
5858
if (!publicUpload && authenticated) {
5959
await VideoService.upload(formData, uploadProgress)
6060
}
61-
handleAlert({ type: 'success', message: "Your upload will be available after the next scan.", open: true })
61+
handleAlert({
62+
type: 'success',
63+
message: 'Your upload will be available shortly',
64+
autohideDuration: 2500,
65+
open: true,
66+
onClose: () => fetchVideos(),
67+
})
6268
} catch (err) {
6369
handleAlert({
6470
type: 'error',

app/client/src/components/admin/VideoCards.js

+16-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,15 @@ import SensorsIcon from '@mui/icons-material/Sensors'
77
import { VideoService } from '../../services'
88
import UploadCard from './UploadCard'
99

10-
const VideoCards = ({ videos, loadingIcon = null, feedView = false, showUploadCard = false, authenticated, size }) => {
10+
const VideoCards = ({
11+
videos,
12+
loadingIcon = null,
13+
feedView = false,
14+
showUploadCard = false,
15+
fetchVideos,
16+
authenticated,
17+
size,
18+
}) => {
1119
const [vids, setVideos] = React.useState(videos)
1220
const [alert, setAlert] = React.useState({ open: false })
1321
const [videoModal, setVideoModal] = React.useState({
@@ -126,7 +134,12 @@ const VideoCards = ({ videos, loadingIcon = null, feedView = false, showUploadCa
126134
authenticated={authenticated}
127135
updateCallback={handleUpdate}
128136
/>
129-
<SnackbarAlert severity={alert.type} open={alert.open} setOpen={(open) => setAlert({ ...alert, open })}>
137+
<SnackbarAlert
138+
severity={alert.type}
139+
open={alert.open}
140+
onClose={alert.onClose}
141+
setOpen={(open) => setAlert({ ...alert, open })}
142+
>
130143
{alert.message}
131144
</SnackbarAlert>
132145

@@ -139,6 +152,7 @@ const VideoCards = ({ videos, loadingIcon = null, feedView = false, showUploadCa
139152
feedView={feedView}
140153
cardWidth={size}
141154
handleAlert={memoizedHandleAlert}
155+
fetchVideos={fetchVideos}
142156
publicUpload={feedView}
143157
/>
144158
)}

app/client/src/components/alert/SnackbarAlert.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,21 @@ import * as React from 'react'
22
import Snackbar from '@mui/material/Snackbar'
33
import Alert from './Alert'
44

5-
export default function SnackbarAlert({ severity, children, open, setOpen }) {
5+
export default function SnackbarAlert({ severity, children, open, autoHideDuration, setOpen, onClose }) {
66
const handleClose = (event, reason) => {
77
if (reason === 'clickaway') {
88
return
99
}
1010
setOpen(false)
11+
if (onClose) {
12+
onClose()
13+
}
1114
}
1215

1316
return (
1417
<Snackbar
1518
open={open}
16-
autoHideDuration={5000}
19+
autoHideDuration={autoHideDuration || 5000}
1720
onClose={handleClose}
1821
anchorOrigin={{
1922
vertical: 'bottom',

app/client/src/views/Dashboard.js

+4-5
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,7 @@ const Dashboard = ({ authenticated, searchText, cardSize, listStyle }) => {
2525
const [selectedFolder, setSelectedFolder] = React.useState(
2626
getSetting('folder') || { value: 'All Videos', label: 'All Videos' },
2727
)
28-
const [selectedSort, setSelectedSort] = React.useState(
29-
getSetting('sortOption') || SORT_OPTIONS[0],
30-
)
28+
const [selectedSort, setSelectedSort] = React.useState(getSetting('sortOption') || SORT_OPTIONS[0])
3129

3230
const [alert, setAlert] = React.useState({ open: false })
3331

@@ -84,12 +82,12 @@ const Dashboard = ({ authenticated, searchText, cardSize, listStyle }) => {
8482
setSetting('folder', folder)
8583
setSelectedFolder(folder)
8684
}
87-
85+
8886
const handleSortSelection = (sortOption) => {
8987
setSetting('sortOption', sortOption)
9088
setSelectedSort(sortOption)
9189
}
92-
90+
9391
return (
9492
<>
9593
<SnackbarAlert severity={alert.type} open={alert.open} setOpen={(open) => setAlert({ ...alert, open })}>
@@ -146,6 +144,7 @@ const Dashboard = ({ authenticated, searchText, cardSize, listStyle }) => {
146144
loadingIcon={loading ? <LoadingSpinner /> : null}
147145
size={cardSize}
148146
showUploadCard={selectedFolder.value === 'All Videos'}
147+
fetchVideos={fetchVideos}
149148
videos={
150149
selectedFolder.value === 'All Videos'
151150
? filteredVideos

app/client/src/views/Feed.js

+4-5
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,7 @@ const Feed = ({ authenticated, searchText, cardSize, listStyle }) => {
3838
? { value: category, label: category }
3939
: getSetting('folder') || { value: 'All Videos', label: 'All Videos' },
4040
)
41-
const [selectedSort, setSelectedSort] = React.useState(
42-
getSetting('sortOption') || SORT_OPTIONS[0],
43-
)
41+
const [selectedSort, setSelectedSort] = React.useState(getSetting('sortOption') || SORT_OPTIONS[0])
4442

4543
const [alert, setAlert] = React.useState({ open: false })
4644

@@ -102,12 +100,12 @@ const Feed = ({ authenticated, searchText, cardSize, listStyle }) => {
102100
window.history.replaceState({ category: folder.value }, '', `/#/feed?${searchParams.toString()}`)
103101
}
104102
}
105-
103+
106104
const handleSortSelection = (sortOption) => {
107105
setSetting('sortOption', sortOption)
108106
setSelectedSort(sortOption)
109107
}
110-
108+
111109
return (
112110
<>
113111
<SnackbarAlert severity={alert.type} open={alert.open} setOpen={(open) => setAlert({ ...alert, open })}>
@@ -167,6 +165,7 @@ const Feed = ({ authenticated, searchText, cardSize, listStyle }) => {
167165
loadingIcon={loading ? <LoadingSpinner /> : null}
168166
feedView={true}
169167
size={cardSize}
168+
fetchVideos={fetchVideos}
170169
showUploadCard={selectedFolder.value === 'All Videos'}
171170
videos={
172171
selectedFolder.value === 'All Videos'

app/server/fireshare/api.py

+11-6
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,8 @@ def public_upload_video():
244244
if not config['app_config']['allow_public_upload']:
245245
logging.warn("A public upload attempt was made but public uploading is disabled")
246246
return Response(status=401)
247+
248+
upload_folder = config['app_config']['public_upload_folder_name']
247249

248250
if 'file' not in request.files:
249251
return Response(status=400)
@@ -254,16 +256,16 @@ def public_upload_video():
254256
filetype = file.filename.split('.')[-1]
255257
if not filetype in SUPPORTED_FILE_TYPES:
256258
return Response(status=400)
257-
upload_directory = paths['video'] / config['app_config']['public_upload_folder_name']
259+
upload_directory = paths['video'] / upload_folder
258260
if not os.path.exists(upload_directory):
259261
os.makedirs(upload_directory)
260262
save_path = os.path.join(upload_directory, filename)
261263
if (os.path.exists(save_path)):
262264
name_no_type = ".".join(filename.split('.')[0:-1])
263265
uid = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(6))
264-
save_path = os.path.join(paths['video'], config['app_config']['public_upload_folder_name'], f"{name_no_type}-{uid}.{filetype}")
266+
save_path = os.path.join(paths['video'], upload_folder, f"{name_no_type}-{uid}.{filetype}")
265267
file.save(save_path)
266-
Popen("fireshare bulk-import", shell=True)
268+
Popen(f"fireshare scan-video --path=\"{save_path}\"", shell=True)
267269
return Response(status=201)
268270

269271
@api.route('/api/upload', methods=['POST'])
@@ -276,6 +278,9 @@ def upload_video():
276278
except:
277279
return Response(status=500, response="Invalid or corrupt config file")
278280
configfile.close()
281+
282+
upload_folder = config['app_config']['admin_upload_folder_name']
283+
279284
if 'file' not in request.files:
280285
return Response(status=400)
281286
file = request.files['file']
@@ -285,16 +290,16 @@ def upload_video():
285290
filetype = file.filename.split('.')[-1]
286291
if not filetype in SUPPORTED_FILE_TYPES:
287292
return Response(status=400)
288-
upload_directory = paths['video'] / config['app_config']['admin_upload_folder_name']
293+
upload_directory = paths['video'] / upload_folder
289294
if not os.path.exists(upload_directory):
290295
os.makedirs(upload_directory)
291296
save_path = os.path.join(upload_directory, filename)
292297
if (os.path.exists(save_path)):
293298
name_no_type = ".".join(filename.split('.')[0:-1])
294299
uid = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(6))
295-
save_path = os.path.join(paths['video'], config['app_config']['admin_upload_folder_name'], f"{name_no_type}-{uid}.{filetype}")
300+
save_path = os.path.join(paths['video'], upload_folder, f"{name_no_type}-{uid}.{filetype}")
296301
file.save(save_path)
297-
Popen("fireshare bulk-import", shell=True)
302+
Popen(f"fireshare scan-video --path=\"{save_path}\"", shell=True)
298303
return Response(status=201)
299304

300305
@api.route('/api/video')

app/server/fireshare/cli.py

+90-11
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,11 @@ def add_user(username, password):
3434
click.echo(f"Created user {username}")
3535

3636
@cli.command()
37-
def scan_videos():
37+
@click.option("--root", "-r", help="root video path to scan", required=False)
38+
def scan_videos(root):
3839
with create_app().app_context():
3940
paths = current_app.config['PATHS']
40-
raw_videos = paths["video"]
41+
videos_path = paths["video"]
4142
video_links = paths["processed"] / "video_links"
4243

4344
config_file = open(paths["data"] / "config.json")
@@ -47,13 +48,13 @@ def scan_videos():
4748
if not video_links.is_dir():
4849
video_links.mkdir()
4950

50-
logger.info(f"Scanning {str(raw_videos)} for {', '.join(SUPPORTED_FILE_EXTENSIONS)} video files")
51-
video_files = [f for f in raw_videos.glob('**/*') if f.is_file() and f.suffix.lower() in SUPPORTED_FILE_EXTENSIONS]
51+
logger.info(f"Scanning {str(videos_path)} for {', '.join(SUPPORTED_FILE_EXTENSIONS)} video files")
52+
video_files = [f for f in (videos_path / root if root else videos_path).glob('**/*') if f.is_file() and f.suffix.lower() in SUPPORTED_FILE_EXTENSIONS]
5253
video_rows = Video.query.all()
5354

5455
new_videos = []
5556
for vf in video_files:
56-
path = str(vf.relative_to(raw_videos))
57+
path = str(vf.relative_to(videos_path))
5758
video_id = util.video_id(vf)
5859
existing = next((vr for vr in video_rows if vr.video_id == video_id), None)
5960
duplicate = next((dvr for dvr in new_videos if dvr.video_id == video_id), None)
@@ -64,16 +65,16 @@ def scan_videos():
6465
logger.info(f"Updating Video {video_id}, available=True")
6566
db.session.query(Video).filter_by(video_id=existing.video_id).update({ "available": True })
6667
if not existing.created_at:
67-
created_at = datetime.fromtimestamp(os.path.getctime(f"{raw_videos}/{path}"))
68+
created_at = datetime.fromtimestamp(os.path.getctime(f"{videos_path}/{path}"))
6869
logger.info(f"Updating Video {video_id}, created_at={created_at}")
6970
db.session.query(Video).filter_by(video_id=existing.video_id).update({ "created_at": created_at })
7071
if not existing.updated_at:
71-
updated_at = datetime.fromtimestamp(os.path.getmtime(f"{raw_videos}/{path}"))
72+
updated_at = datetime.fromtimestamp(os.path.getmtime(f"{videos_path}/{path}"))
7273
logger.info(f"Updating Video {video_id}, updated_at={updated_at}")
7374
db.session.query(Video).filter_by(video_id=existing.video_id).update({ "updated_at": updated_at })
7475
else:
75-
created_at = datetime.fromtimestamp(os.path.getctime(f"{raw_videos}/{path}"))
76-
updated_at = datetime.fromtimestamp(os.path.getmtime(f"{raw_videos}/{path}"))
76+
created_at = datetime.fromtimestamp(os.path.getctime(f"{videos_path}/{path}"))
77+
updated_at = datetime.fromtimestamp(os.path.getmtime(f"{videos_path}/{path}"))
7778
v = Video(video_id=video_id, extension=vf.suffix, path=path, available=True, created_at=created_at, updated_at=updated_at)
7879
logger.info(f"Adding new Video {video_id} at {str(path)} (created {created_at.isoformat()}, updated {updated_at.isoformat()})")
7980
new_videos.append(v)
@@ -112,6 +113,83 @@ def scan_videos():
112113
db.session.query(Video).filter_by(video_id=ev.video_id).update({ "available": False})
113114
db.session.commit()
114115

116+
@cli.command()
117+
@click.option("--path", "-p", help="path to video to scan", required=False)
118+
def scan_video(path):
119+
with create_app().app_context():
120+
paths = current_app.config['PATHS']
121+
videos_path = paths["video"]
122+
video_links = paths["processed"] / "video_links"
123+
124+
config_file = open(paths["data"] / "config.json")
125+
video_config = json.load(config_file)["app_config"]["video_defaults"]
126+
config_file.close()
127+
128+
if not video_links.is_dir():
129+
video_links.mkdir()
130+
131+
video_file = (videos_path / path) if (videos_path / path).is_file() and (videos_path / path).suffix.lower() in SUPPORTED_FILE_EXTENSIONS else None
132+
if video_file:
133+
video_rows = Video.query.all()
134+
logger.info(f"Scanning {str(video_file)}")
135+
136+
path = str(video_file.relative_to(videos_path))
137+
video_id = util.video_id(video_file)
138+
existing = next((vr for vr in video_rows if vr.video_id == video_id), None)
139+
if existing:
140+
if not existing.available:
141+
logger.info(f"Updating Video {video_id}, available=True")
142+
db.session.query(Video).filter_by(video_id=existing.video_id).update({ "available": True })
143+
if not existing.created_at:
144+
created_at = datetime.fromtimestamp(os.path.getctime(f"{videos_path}/{path}"))
145+
logger.info(f"Updating Video {video_id}, created_at={created_at}")
146+
db.session.query(Video).filter_by(video_id=existing.video_id).update({ "created_at": created_at })
147+
if not existing.updated_at:
148+
updated_at = datetime.fromtimestamp(os.path.getmtime(f"{videos_path}/{path}"))
149+
logger.info(f"Updating Video {video_id}, updated_at={updated_at}")
150+
db.session.query(Video).filter_by(video_id=existing.video_id).update({ "updated_at": updated_at })
151+
else:
152+
created_at = datetime.fromtimestamp(os.path.getctime(f"{videos_path}/{path}"))
153+
updated_at = datetime.fromtimestamp(os.path.getmtime(f"{videos_path}/{path}"))
154+
v = Video(video_id=video_id, extension=video_file.suffix, path=path, available=True, created_at=created_at, updated_at=updated_at)
155+
logger.info(f"Adding new Video {video_id} at {str(path)} (created {created_at.isoformat()}, updated {updated_at.isoformat()})")
156+
db.session.add(v)
157+
fd = os.open(str(video_links.absolute()), os.O_DIRECTORY)
158+
src = Path((paths["video"] / v.path).absolute())
159+
dst = Path(paths["processed"] / "video_links" / (video_id + video_file.suffix))
160+
common_root = Path(*os.path.commonprefix([src.parts, dst.parts]))
161+
num_up = len(dst.parts)-1 - len(common_root.parts)
162+
prefix = "../" * num_up
163+
rel_src = Path(prefix + str(src).replace(str(common_root), ''))
164+
if not dst.exists():
165+
logger.info(f"Linking {str(rel_src)} --> {str(dst)}")
166+
try:
167+
os.symlink(src, dst, dir_fd=fd)
168+
except FileExistsError:
169+
logger.info(f"{dst} exists already")
170+
info = VideoInfo(video_id=v.video_id, title=Path(v.path).stem, private=video_config["private"])
171+
db.session.add(info)
172+
173+
processed_root = Path(current_app.config['PROCESSED_DIRECTORY'])
174+
logger.info(f"Checking for videos with missing posters...")
175+
derived_path = Path(processed_root, "derived", info.video_id)
176+
video_path = Path(processed_root, "video_links", info.video_id + video_file.suffix)
177+
if video_path.exists():
178+
poster_path = Path(derived_path, "poster.jpg")
179+
should_create_poster = (not poster_path.exists() or regenerate)
180+
if should_create_poster:
181+
if not derived_path.exists():
182+
derived_path.mkdir(parents=True)
183+
poster_time = 0
184+
util.create_poster(video_path, derived_path / "poster.jpg", poster_time)
185+
else:
186+
logger.debug(f"Skipping creation of poster for video {info.video_id} because it exists at {str(poster_path)}")
187+
db.session.commit()
188+
else:
189+
logger.warn(f"Skipping creation of poster for video {info.video_id} because the video at {str(video_path)} does not exist or is not accessible")
190+
else:
191+
logger.info(f"Invalid video file, unable to scan: {str(videos_path / path)}")
192+
115193
@cli.command()
116194
def repair_symlinks():
117195
with create_app().app_context():
@@ -265,7 +343,8 @@ def create_boomerang_posters(regenerate):
265343

266344
@cli.command()
267345
@click.pass_context
268-
def bulk_import(ctx):
346+
@click.option("--root", "-r", help="root video path to scan", required=False)
347+
def bulk_import(ctx, root):
269348
with create_app().app_context():
270349
paths = current_app.config['PATHS']
271350
if util.lock_exists(paths["data"]):
@@ -275,7 +354,7 @@ def bulk_import(ctx):
275354

276355
timing = {}
277356
s = time.time()
278-
ctx.invoke(scan_videos)
357+
ctx.invoke(scan_videos, root=root)
279358
timing['scan_videos'] = time.time() - s
280359
s = time.time()
281360
ctx.invoke(sync_metadata)

0 commit comments

Comments
 (0)