Compare commits

...

6 Commits

Author SHA1 Message Date
Markos Gogoulos
02eb712a50 fix 2025-10-25 17:51:09 +03:00
Markos Gogoulos
8c73633429 fix 2025-10-25 17:18:46 +03:00
Markos Gogoulos
496285e9e1 fix 2025-10-25 17:08:28 +03:00
Markos Gogoulos
55c5b0be12 wtv 2025-10-25 16:11:26 +03:00
Markos Gogoulos
3c74badaec a 2025-10-25 15:37:16 +03:00
Markos Gogoulos
030e3cbe68 index subtitles too 2025-10-25 14:08:33 +03:00
22 changed files with 179 additions and 41 deletions

View File

@ -1 +1 @@
VERSION = "7.1.0"
VERSION = "7.3.0"

View File

@ -357,6 +357,10 @@ class Media(models.Model):
a_tags,
b_tags,
]
for subtitle in self.subtitles.all():
items.append(subtitle.subtitle_text)
items = [item for item in items if item]
text = " ".join(items)
text = " ".join([token for token in text.lower().split(" ") if token not in STOP_WORDS])
@ -406,11 +410,11 @@ class Media(models.Model):
self.media_type = "image"
elif kind == "pdf":
self.media_type = "pdf"
if self.media_type in ["audio", "image", "pdf"]:
if self.media_type in ["image", "pdf"]:
self.encoding_status = "success"
else:
ret = helpers.media_file_info(self.media_file.path)
if ret.get("fail"):
self.media_type = ""
self.encoding_status = "fail"

View File

@ -1,6 +1,7 @@
import os
import tempfile
import pysubs2
from django.conf import settings
from django.db import models
from django.urls import reverse
@ -73,6 +74,17 @@ class Subtitle(models.Model):
raise Exception("Could not convert to srt")
return True
@property
def subtitle_text(self):
sub = pysubs2.load(self.subtitle_file.path, encoding="utf-8")
text = ' '.join([line.text for line in sub])
text = text.replace("\\N", " ")
text = text.replace("-", " ")
text = text.replace(".", " ")
text = text.replace(" ", " ")
return text
class TranscriptionRequest(models.Model):
# Whisper transcription request

View File

@ -1003,7 +1003,6 @@ def video_trim_task(self, trim_request_id):
timestamps_encodings = get_trim_timestamps(trim_request.media.trim_video_path, trim_request.timestamps)
timestamps_original = get_trim_timestamps(trim_request.media.media_file.path, trim_request.timestamps)
if not timestamps_encodings:
trim_request.status = "fail"
trim_request.save(update_fields=["status"])

View File

@ -174,15 +174,15 @@ class MediaList(APIView):
media = Media.objects.none()
else:
base_queryset = Media.objects.prefetch_related("user", "tags")
user_media_filters = {'permissions__user': request.user}
media = base_queryset.filter(**user_media_filters)
# Build OR conditions similar to _get_media_queryset
conditions = Q(permissions__user=request.user)
if getattr(settings, 'USE_RBAC', False):
rbac_categories = request.user.get_rbac_categories_as_member()
rbac_filters = {'category__in': rbac_categories}
conditions |= Q(category__in=rbac_categories)
rbac_media = base_queryset.filter(**rbac_filters)
media = media.union(rbac_media)
media = base_queryset.filter(conditions).distinct()
elif author_param:
user_queryset = User.objects.all()
user = get_object_or_404(user_queryset, username=author_param)
@ -221,13 +221,12 @@ class MediaList(APIView):
if publish_state and publish_state in ['private', 'public', 'unlisted']:
media = media.filter(state=publish_state)
if show_param == "shared_with_me":
media = media[:1000] # limit to 1000 results
already_sorted = True
if not already_sorted:
media = media.order_by(f"{ordering}{sort_by}")
if show_param == "shared_with_me":
media = media[:1000] # limit to 1000 results
paginator = pagination_class()
page = paginator.paginate_queryset(media, request)

View File

@ -430,7 +430,7 @@ def edit_video(request):
return HttpResponseRedirect("/")
if media.media_type not in ["video", "audio"]:
messages.add_message(request, messages.INFO, "Media is not video")
messages.add_message(request, messages.INFO, "Media is not video or audio")
return HttpResponseRedirect(media.get_absolute_url())
if not settings.ALLOW_VIDEO_TRIMMER:

View File

@ -87,7 +87,7 @@ export const BulkActionPermissionModal: React.FC<BulkActionPermissionModalProps>
}
try {
const response = await fetch(`/api/v1/users?name=${encodeURIComponent(name)}`);
const response = await fetch(`/api/v1/users?name=${encodeURIComponent(name)}&exclude_self=True`);
if (!response.ok) {
throw new Error(translateString('Failed to search users'));
}
@ -110,6 +110,12 @@ export const BulkActionPermissionModal: React.FC<BulkActionPermissionModalProps>
clearTimeout(searchTimeout);
}
// Only search if 3 or more characters
if (value.trim().length < 3) {
setSearchResults([]);
return;
}
// Set new timeout for debounced search
const timeout = setTimeout(() => {
searchUsers(value);
@ -231,7 +237,7 @@ export const BulkActionPermissionModal: React.FC<BulkActionPermissionModalProps>
<div className="search-box">
<input
type="text"
placeholder={translateString('Search users to add...')}
placeholder={translateString('Search users to add (min 3 characters)...')}
value={addSearchTerm}
onChange={(e) => handleAddSearchChange(e.target.value)}
/>

View File

@ -696,6 +696,11 @@ a.item-edit-link {
opacity: 1;
}
// Show all checkboxes when any item has a selection
&.has-any-selection .item-selection-checkbox {
opacity: 1;
}
// Add hover shadow when any selection is active
&.has-any-selection:not(.selected):hover {
.item-content {

View File

@ -27,15 +27,33 @@ export function MediaItem(props) {
(props.isSelected ? ' selected' : '') +
(props.hasAnySelection ? ' has-any-selection' : '');
const handleItemClick = (e) => {
// If there's any selection active, clicking the item should toggle selection
if (props.hasAnySelection && props.onCheckboxChange) {
// Check if clicking on the checkbox itself, edit icon, or view icon
if (e.target.closest('.item-selection-checkbox') ||
e.target.closest('.item-edit-icon') ||
e.target.closest('.item-view-icon')) {
return; // Let these elements handle their own clicks
}
// Prevent all other clicks and toggle selection
e.preventDefault();
e.stopPropagation();
props.onCheckboxChange({ target: { checked: !props.isSelected } });
}
};
return (
<div className={finalClassname}>
<div className={finalClassname} onClick={handleItemClick}>
<div className="item-content">
{props.showSelection && (
<div className="item-selection-checkbox">
<div className="item-selection-checkbox" onClick={(e) => e.stopPropagation()}>
<input
type="checkbox"
checked={props.isSelected || false}
onChange={(e) => { props.onCheckboxChange && props.onCheckboxChange(e); }}
onClick={(e) => e.stopPropagation()}
aria-label="Select media"
/>
</div>

View File

@ -70,8 +70,25 @@ export function MediaItemAudio(props) {
(props.isSelected ? ' selected' : '') +
(props.hasAnySelection ? ' has-any-selection' : '');
const handleItemClick = (e) => {
// If there's any selection active, clicking the item should toggle selection
if (props.hasAnySelection && props.onCheckboxChange) {
// Check if clicking on the checkbox itself, edit icon, or view icon
if (e.target.closest('.item-selection-checkbox') ||
e.target.closest('.item-edit-icon') ||
e.target.closest('.item-view-icon')) {
return; // Let these elements handle their own clicks
}
// Prevent all other clicks and toggle selection
e.preventDefault();
e.stopPropagation();
props.onCheckboxChange({ target: { checked: !props.isSelected } });
}
};
return (
<div className={finalClassname}>
<div className={finalClassname} onClick={handleItemClick}>
{playlistOrderNumberComponent()}
<div className="item-content">

View File

@ -77,8 +77,25 @@ export function MediaItemVideo(props) {
(props.isSelected ? ' selected' : '') +
(props.hasAnySelection ? ' has-any-selection' : '');
const handleItemClick = (e) => {
// If there's any selection active, clicking the item should toggle selection
if (props.hasAnySelection && props.onCheckboxChange) {
// Check if clicking on the checkbox itself, edit icon, or view icon
if (e.target.closest('.item-selection-checkbox') ||
e.target.closest('.item-edit-icon') ||
e.target.closest('.item-view-icon')) {
return; // Let these elements handle their own clicks
}
// Prevent all other clicks and toggle selection
e.preventDefault();
e.stopPropagation();
props.onCheckboxChange({ target: { checked: !props.isSelected } });
}
};
return (
<div className={finalClassname}>
<div className={finalClassname} onClick={handleItemClick}>
{playlistOrderNumberComponent()}
<div className="item-content">

View File

@ -414,10 +414,22 @@ class NavMenuInlineTabs extends React.PureComponent {
) : null}
{this.props.onToggleSortingClick && ['media', 'shared_by_me', 'shared_with_me'].includes(this.props.type) ? (
<li className="media-sorting-toggle">
<span style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }} onClick={this.props.onToggleSortingClick} title={translateString('Sort By')}>
<span style={{ display: 'flex', alignItems: 'center', cursor: 'pointer', position: 'relative' }} onClick={this.props.onToggleSortingClick} title={translateString('Sort By')}>
<CircleIconButton buttonShadow={false}>
<i className="material-icons">swap_vert</i>
</CircleIconButton>
{this.props.hasActiveSort ? (
<span style={{
position: 'absolute',
top: '8px',
right: '8px',
width: '8px',
height: '8px',
borderRadius: '50%',
backgroundColor: 'var(--default-theme-color)',
border: '2px solid white',
}}></span>
) : null}
</span>
</li>
) : null}
@ -438,6 +450,7 @@ NavMenuInlineTabs.propTypes = {
onToggleSortingClick: PropTypes.func,
hasActiveFilters: PropTypes.bool,
hasActiveTags: PropTypes.bool,
hasActiveSort: PropTypes.bool,
};
function AddBannerButton(props) {
@ -663,6 +676,7 @@ export default function ProfilePagesHeader(props) {
onToggleSortingClick={props.onToggleSortingClick}
hasActiveFilters={props.hasActiveFilters}
hasActiveTags={props.hasActiveTags}
hasActiveSort={props.hasActiveSort}
/>
</div>
</div>
@ -678,6 +692,7 @@ ProfilePagesHeader.propTypes = {
onToggleSortingClick: PropTypes.func,
hasActiveFilters: PropTypes.bool,
hasActiveTags: PropTypes.bool,
hasActiveSort: PropTypes.bool,
};
ProfilePagesHeader.defaultProps = {

View File

@ -67,6 +67,9 @@ export function ProfileMediaFilters(props) {
}
function onFilterSelect(ev) {
const filterType = ev.currentTarget.getAttribute('filter');
const clickedValue = ev.currentTarget.getAttribute('value');
const args = {
media_type: mediaTypeFilter,
upload_date: uploadDateFilter,
@ -76,34 +79,35 @@ export function ProfileMediaFilters(props) {
tag: props.selectedTag || tagFilter,
};
switch (ev.currentTarget.getAttribute('filter')) {
switch (filterType) {
case 'media_type':
args.media_type = ev.currentTarget.getAttribute('value');
// If clicking the currently selected filter, deselect it (set to 'all')
args.media_type = clickedValue === mediaTypeFilter ? 'all' : clickedValue;
props.onFiltersUpdate(args);
setFilter_media_type(args.media_type);
break;
case 'upload_date':
args.upload_date = ev.currentTarget.getAttribute('value');
args.upload_date = clickedValue === uploadDateFilter ? 'all' : clickedValue;
props.onFiltersUpdate(args);
setFilter_upload_date(args.upload_date);
break;
case 'duration':
args.duration = ev.currentTarget.getAttribute('value');
args.duration = clickedValue === durationFilter ? 'all' : clickedValue;
props.onFiltersUpdate(args);
setFilter_duration(args.duration);
break;
case 'publish_state':
args.publish_state = ev.currentTarget.getAttribute('value');
args.publish_state = clickedValue === publishStateFilter ? 'all' : clickedValue;
props.onFiltersUpdate(args);
setFilter_publish_state(args.publish_state);
break;
case 'sort_by':
args.sort_by = ev.currentTarget.getAttribute('value');
args.sort_by = clickedValue === sortByFilter ? 'date_added_desc' : clickedValue;
props.onFiltersUpdate(args);
setFilter_sort_by(args.sort_by);
break;
case 'tag':
args.tag = ev.currentTarget.getAttribute('value');
args.tag = clickedValue === tagFilter ? 'all' : clickedValue;
props.onFiltersUpdate(args);
setFilter_tag(args.tag);
break;

View File

@ -26,8 +26,10 @@ export function ProfileMediaTags(props) {
function onFilterSelect(ev) {
const tag = ev.currentTarget.getAttribute('value');
setFilter_tag(tag);
props.onTagSelect(tag);
// If clicking the currently selected tag, deselect it (set to 'all')
const newTag = tag === tagFilter ? 'all' : tag;
setFilter_tag(newTag);
props.onTagSelect(newTag);
}
useEffect(() => {

View File

@ -605,7 +605,7 @@ export class ProfileMediaPage extends Page {
}
onTagSelect(tag) {
this.setState({ selectedTag: tag, hiddenTags: true }, () => {
this.setState({ selectedTag: tag }, () => {
// Apply tag filter
this.onFiltersUpdate({
media_type: this.state.filterArgs.includes('media_type') ? this.state.filterArgs.match(/media_type=([^&]*)/)?.[1] : null,
@ -617,7 +617,7 @@ export class ProfileMediaPage extends Page {
}
onSortSelect(sortOption) {
this.setState({ selectedSort: sortOption, hiddenSorting: true }, () => {
this.setState({ selectedSort: sortOption }, () => {
// Apply sort filter
this.onFiltersUpdate({
media_type: this.state.filterArgs.includes('media_type') ? this.state.filterArgs.match(/media_type=([^&]*)/)?.[1] : null,
@ -884,12 +884,15 @@ export class ProfileMediaPage extends Page {
);
const hasActiveTags = this.state.selectedTag && this.state.selectedTag !== 'all';
const hasActiveSort = this.state.selectedSort && this.state.selectedSort !== 'date_added_desc';
console.log('Filter Debug:', {
filterArgs: this.state.filterArgs,
selectedTag: this.state.selectedTag,
selectedSort: this.state.selectedSort,
hasActiveFilters,
hasActiveTags
hasActiveTags,
hasActiveSort
});
return [
@ -904,6 +907,7 @@ export class ProfileMediaPage extends Page {
onToggleSortingClick={this.onToggleSortingClick}
hasActiveFilters={hasActiveFilters}
hasActiveTags={hasActiveTags}
hasActiveSort={hasActiveSort}
/>
) : null,
this.state.author ? (

View File

@ -151,17 +151,23 @@ export class ProfileSharedByMePage extends Page {
onToggleFiltersClick() {
this.setState({
hiddenFilters: !this.state.hiddenFilters,
hiddenTags: true,
hiddenSorting: true,
});
}
onToggleTagsClick() {
this.setState({
hiddenFilters: true,
hiddenTags: !this.state.hiddenTags,
hiddenSorting: true,
});
}
onToggleSortingClick() {
this.setState({
hiddenFilters: true,
hiddenTags: true,
hiddenSorting: !this.state.hiddenSorting,
});
}
@ -290,6 +296,12 @@ export class ProfileSharedByMePage extends Page {
const isMediaAuthor = authorData && authorData.username === MemberContext._currentValue.username;
// Check if any filters are active
const hasActiveFilters = this.state.filterArgs && (
this.state.filterArgs.includes('media_type=') ||
this.state.filterArgs.includes('upload_date=')
);
return [
this.state.author ? (
<ProfilePagesHeader
@ -300,6 +312,9 @@ export class ProfileSharedByMePage extends Page {
onToggleFiltersClick={this.onToggleFiltersClick}
onToggleTagsClick={this.onToggleTagsClick}
onToggleSortingClick={this.onToggleSortingClick}
hasActiveFilters={hasActiveFilters}
hasActiveTags={this.state.selectedTag !== 'all'}
hasActiveSort={this.state.selectedSort !== 'date_added_desc'}
/>
) : null,
this.state.author ? (

View File

@ -151,17 +151,23 @@ export class ProfileSharedWithMePage extends Page {
onToggleFiltersClick() {
this.setState({
hiddenFilters: !this.state.hiddenFilters,
hiddenTags: true,
hiddenSorting: true,
});
}
onToggleTagsClick() {
this.setState({
hiddenFilters: true,
hiddenTags: !this.state.hiddenTags,
hiddenSorting: true,
});
}
onToggleSortingClick() {
this.setState({
hiddenFilters: true,
hiddenTags: true,
hiddenSorting: !this.state.hiddenSorting,
});
}
@ -290,6 +296,12 @@ export class ProfileSharedWithMePage extends Page {
const isMediaAuthor = authorData && authorData.username === MemberContext._currentValue.username;
// Check if any filters are active
const hasActiveFilters = this.state.filterArgs && (
this.state.filterArgs.includes('media_type=') ||
this.state.filterArgs.includes('upload_date=')
);
return [
this.state.author ? (
<ProfilePagesHeader
@ -300,6 +312,9 @@ export class ProfileSharedWithMePage extends Page {
onToggleFiltersClick={this.onToggleFiltersClick}
onToggleTagsClick={this.onToggleTagsClick}
onToggleSortingClick={this.onToggleSortingClick}
hasActiveFilters={hasActiveFilters}
hasActiveTags={this.state.selectedTag !== 'all'}
hasActiveSort={this.state.selectedSort !== 'date_added_desc'}
/>
) : null,
this.state.author ? (

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -206,6 +206,7 @@ class UserList(APIView):
manual_parameters=[
openapi.Parameter(name='page', type=openapi.TYPE_INTEGER, in_=openapi.IN_QUERY, description='Page number'),
openapi.Parameter(name='name', type=openapi.TYPE_STRING, in_=openapi.IN_QUERY, description='Search by name or username'),
openapi.Parameter(name='exclude_self', type=openapi.TYPE_BOOLEAN, in_=openapi.IN_QUERY, description='Exclude current user from results'),
],
tags=['Users'],
operation_summary='List users',
@ -226,6 +227,11 @@ class UserList(APIView):
if name:
users = users.filter(Q(name__icontains=name) | Q(username__icontains=name))
# Exclude current user if requested
exclude_self = request.GET.get("exclude_self", "") == "True"
if exclude_self and request.user.is_authenticated:
users = users.exclude(id=request.user.id)
if settings.USERS_NEEDS_TO_BE_APPROVED:
is_approved = request.GET.get("is_approved")
if is_approved == "true":