add packeges and youtube
This commit is contained in:
parent
c673d9b2b1
commit
f4cb2d0af8
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -2,7 +2,7 @@ from django.contrib import admin
|
||||
from django import forms
|
||||
from django.utils.html import format_html
|
||||
from django.urls import reverse
|
||||
from .models import Classroom, Teacher, Subject, Resource, Student, City, Governorate
|
||||
from .models import Classroom, Teacher, Subject, Resource, Student, City, Governorate, Package
|
||||
|
||||
class ActionsModelAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
@ -93,3 +93,11 @@ class StudentAdmin(ActionsModelAdmin):
|
||||
if obj and obj.classroom:
|
||||
form.base_fields['subscribed_subjects'].queryset = Subject.objects.filter(classroom=obj.classroom)
|
||||
return form
|
||||
|
||||
@admin.register(Package)
|
||||
class PackageAdmin(ActionsModelAdmin):
|
||||
list_display = ('name_en', 'name_ar', 'classroom', 'price', 'is_active', 'actions_column')
|
||||
list_filter = ('classroom', 'is_active')
|
||||
list_editable = ('is_active',)
|
||||
filter_horizontal = ('subjects',)
|
||||
search_fields = ('name_en', 'name_ar')
|
||||
|
||||
31
core/migrations/0015_package.py
Normal file
31
core/migrations/0015_package.py
Normal file
@ -0,0 +1,31 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-04 15:24
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0014_alter_city_options_alter_classroom_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Package',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name_en', models.CharField(max_length=200, verbose_name='الاسم (إنجليزي)')),
|
||||
('name_ar', models.CharField(max_length=200, verbose_name='الاسم (عربي)')),
|
||||
('description_en', models.TextField(blank=True, verbose_name='الوصف (إنجليزي)')),
|
||||
('description_ar', models.TextField(blank=True, verbose_name='الوصف (عربي)')),
|
||||
('price', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='السعر')),
|
||||
('image', models.ImageField(blank=True, null=True, upload_to='packages/', verbose_name='الصورة')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='نشط')),
|
||||
('subjects', models.ManyToManyField(related_name='packages', to='core.subject', verbose_name='المواد')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'باقة',
|
||||
'verbose_name_plural': 'الباقات',
|
||||
},
|
||||
),
|
||||
]
|
||||
19
core/migrations/0016_package_classroom.py
Normal file
19
core/migrations/0016_package_classroom.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-04 16:14
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0015_package'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='package',
|
||||
name='classroom',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='packages', to='core.classroom', verbose_name='الصف'),
|
||||
),
|
||||
]
|
||||
BIN
core/migrations/__pycache__/0015_package.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0015_package.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
@ -65,18 +65,24 @@ class Subject(models.Model):
|
||||
"""Extracts the video ID from a YouTube URL."""
|
||||
if not self.youtube_live_url:
|
||||
return None
|
||||
|
||||
url = self.youtube_live_url.strip()
|
||||
|
||||
# Handle various formats:
|
||||
# https://youtu.be/VIDEO_ID
|
||||
# https://www.youtube.com/watch?v=VIDEO_ID
|
||||
# https://www.youtube.com/live/VIDEO_ID
|
||||
import re
|
||||
# https://www.youtube.com/embed/VIDEO_ID
|
||||
# https://www.youtube.com/shorts/VIDEO_ID
|
||||
patterns = [
|
||||
r'(?:v=|\/)([0-9A-Za-z_-]{11}).*',
|
||||
r'(?:v=|\/)([0-9A-Za-z_-]{11})(?:[?&]|$)',
|
||||
r'(?:youtu\.be\/)([0-9A-Za-z_-]{11})',
|
||||
r'(?:live\/)([0-9A-Za-z_-]{11})',
|
||||
r'(?:embed\/)([0-9A-Za-z_-]{11})',
|
||||
r'(?:shorts\/)([0-9A-Za-z_-]{11})',
|
||||
]
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, self.youtube_live_url)
|
||||
match = re.search(pattern, url)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return None
|
||||
@ -107,14 +113,18 @@ class Resource(models.Model):
|
||||
"""Extracts the video ID from the link if it is a YouTube URL."""
|
||||
if not self.link or self.resource_type != 'VIDEO':
|
||||
return None
|
||||
|
||||
url = self.link.strip()
|
||||
|
||||
patterns = [
|
||||
r'(?:v=|\/)([0-9A-Za-z_-]{11}).*',
|
||||
r'(?:v=|\/)([0-9A-Za-z_-]{11})(?:[?&]|$)',
|
||||
r'(?:youtu\.be\/)([0-9A-Za-z_-]{11})',
|
||||
r'(?:live\/)([0-9A-Za-z_-]{11})',
|
||||
r'(?:embed\/)([0-9A-Za-z_-]{11})',
|
||||
r'(?:shorts\/)([0-9A-Za-z_-]{11})',
|
||||
]
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, self.link)
|
||||
match = re.search(pattern, url)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return None
|
||||
@ -219,3 +229,21 @@ class PlatformSettings(SingletonModel):
|
||||
|
||||
def __str__(self):
|
||||
return "إعدادات المنصة"
|
||||
|
||||
class Package(models.Model):
|
||||
name_en = models.CharField("الاسم (إنجليزي)", max_length=200)
|
||||
name_ar = models.CharField("الاسم (عربي)", max_length=200)
|
||||
description_en = models.TextField("الوصف (إنجليزي)", blank=True)
|
||||
description_ar = models.TextField("الوصف (عربي)", blank=True)
|
||||
price = models.DecimalField("السعر", max_digits=10, decimal_places=2, default=0.00)
|
||||
classroom = models.ForeignKey(Classroom, on_delete=models.CASCADE, related_name='packages', verbose_name="الصف", null=True, blank=True)
|
||||
image = models.ImageField("الصورة", upload_to='packages/', blank=True, null=True)
|
||||
subjects = models.ManyToManyField(Subject, related_name='packages', verbose_name="المواد")
|
||||
is_active = models.BooleanField("نشط", default=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "باقة"
|
||||
verbose_name_plural = "الباقات"
|
||||
|
||||
def __str__(self):
|
||||
return self.name_ar or self.name_en
|
||||
@ -28,10 +28,11 @@
|
||||
<iframe
|
||||
width="100%"
|
||||
height="100%"
|
||||
src="https://www.youtube.com/embed/{{ youtube_id }}?autoplay=1"
|
||||
src="https://www.youtube-nocookie.com/embed/{{ youtube_id }}?autoplay=1&playsinline=1"
|
||||
title="YouTube video player"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
referrerpolicy="strict-origin-when-cross-origin"
|
||||
allowfullscreen>
|
||||
</iframe>
|
||||
</div>
|
||||
|
||||
@ -151,6 +151,51 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Available Packages Section -->
|
||||
{% if packages %}
|
||||
<div class="mb-4">
|
||||
<h4 class="fw-bold mb-3">الباقات المتاحة 🎁</h4>
|
||||
<div class="row g-4">
|
||||
{% for package in packages %}
|
||||
<div class="col-md-6">
|
||||
<div class="glass-card p-0 overflow-hidden h-100" style="border: 2px solid #ffc107;">
|
||||
{% if package.image %}
|
||||
<div style="height: 150px; overflow: hidden;">
|
||||
<img src="{{ package.image.url }}" class="w-100 h-100 object-fit-cover" alt="{{ package.name_en }}">
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="bg-warning bg-opacity-25 d-flex align-items-center justify-content-center" style="height: 150px;">
|
||||
<span class="text-muted display-4">📦</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="p-3">
|
||||
<h5 class="fw-bold mb-2">{{ package.name_ar }}</h5>
|
||||
<p class="text-muted small mb-3">{{ package.description_ar|truncatechars:100 }}</p>
|
||||
|
||||
<div class="mb-2">
|
||||
<small class="text-muted">تشمل المواد:</small>
|
||||
<div class="d-flex flex-wrap gap-1 mt-1">
|
||||
{% for subject in package.subjects.all|slice:":3" %}
|
||||
<span class="badge bg-secondary bg-opacity-10 text-dark">{{ subject.name_ar }}</span>
|
||||
{% endfor %}
|
||||
{% if package.subjects.count > 3 %}
|
||||
<span class="badge bg-secondary bg-opacity-10 text-dark">+{{ package.subjects.count|add:"-3" }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||
<span class="fw-bold text-success fs-5">{{ package.price }} OMR</span>
|
||||
<a href="{% url 'subscribe_package' package.id %}" class="btn btn-sm btn-warning text-dark fw-bold rounded-pill">شراء الباقة</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Available Subjects Section -->
|
||||
<div class="mb-4">
|
||||
<h4 class="fw-bold mb-3">المواد المتاحة</h4>
|
||||
|
||||
@ -65,13 +65,14 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if subject.youtube_live_url %}
|
||||
{% if subject.get_youtube_id %}
|
||||
<div class="mt-5 mb-5">
|
||||
<div class="ratio ratio-16x9 rounded-4 overflow-hidden shadow-sm">
|
||||
<iframe src="https://www.youtube.com/embed/{{ subject.get_youtube_id }}"
|
||||
<iframe src="https://www.youtube-nocookie.com/embed/{{ subject.get_youtube_id }}?rel=0&playsinline=1"
|
||||
title="YouTube video player"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
referrerpolicy="strict-origin-when-cross-origin"
|
||||
allowfullscreen>
|
||||
</iframe>
|
||||
</div>
|
||||
@ -255,9 +256,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
modalBody.innerHTML = ''; // Clear previous content
|
||||
|
||||
if (type === 'VIDEO' && youtubeId) {
|
||||
// Use youtube-nocookie.com and add referrerpolicy
|
||||
modalBody.innerHTML = `
|
||||
<div class="ratio ratio-16x9 h-100">
|
||||
<iframe src="https://www.youtube.com/embed/${youtubeId}?autoplay=1" title="${title}" allowfullscreen></iframe>
|
||||
<iframe src="https://www.youtube-nocookie.com/embed/${youtubeId}?autoplay=1&rel=0&playsinline=1"
|
||||
title="${title}"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
referrerpolicy="strict-origin-when-cross-origin"
|
||||
allowfullscreen></iframe>
|
||||
</div>
|
||||
`;
|
||||
} else if (type === 'FILE' && fileUrl) {
|
||||
|
||||
@ -20,7 +20,7 @@ class ThawaniClient:
|
||||
else:
|
||||
self.base_url = "https://checkout.thawani.om/api/v1"
|
||||
|
||||
def create_checkout_session(self, subject, user, success_url, cancel_url):
|
||||
def create_checkout_session(self, product, user, success_url, cancel_url):
|
||||
if not self.api_key:
|
||||
raise Exception("Thawani API Key is not configured.")
|
||||
|
||||
@ -31,24 +31,32 @@ class ThawaniClient:
|
||||
}
|
||||
|
||||
# Thawani expects amount in Baisa (1 OMR = 1000 Baisa)
|
||||
amount_baisa = int(subject.price * 1000)
|
||||
amount_baisa = int(product.price * 1000)
|
||||
|
||||
metadata = {
|
||||
"user_id": str(user.id)
|
||||
}
|
||||
|
||||
# Handle Subject or Package
|
||||
# Package has 'subjects' ManyToMany field
|
||||
if hasattr(product, 'subjects'):
|
||||
metadata["package_id"] = str(product.id)
|
||||
else:
|
||||
metadata["subject_id"] = str(product.id)
|
||||
|
||||
payload = {
|
||||
"client_reference_id": str(user.id),
|
||||
"mode": "payment",
|
||||
"products": [
|
||||
{
|
||||
"name": subject.name_en,
|
||||
"name": product.name_en,
|
||||
"quantity": 1,
|
||||
"unit_amount": amount_baisa
|
||||
}
|
||||
],
|
||||
"success_url": success_url,
|
||||
"cancel_url": cancel_url,
|
||||
"metadata": {
|
||||
"subject_id": str(subject.id),
|
||||
"user_id": str(user.id)
|
||||
}
|
||||
"metadata": metadata
|
||||
}
|
||||
|
||||
response = httpx.post(url, json=payload, headers=headers)
|
||||
@ -66,4 +74,4 @@ class ThawaniClient:
|
||||
|
||||
response = httpx.get(url, headers=headers)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
return response.json()
|
||||
@ -16,6 +16,7 @@ urlpatterns = [
|
||||
path('profile/edit/teacher/', views.edit_teacher_profile, name='edit_teacher_profile'),
|
||||
path('logout/', views.custom_logout, name='logout'),
|
||||
path('subscribe/<int:subject_id>/', views.subscribe_subject, name='subscribe_subject'),
|
||||
path('package/<int:package_id>/subscribe/', views.subscribe_package, name='subscribe_package'),
|
||||
path('payment/success/', views.payment_success, name='payment_success'),
|
||||
path('payment/cancel/', views.payment_cancel, name='payment_cancel'),
|
||||
path('classroom/<int:subject_id>/', views.live_classroom, name='live_classroom'),
|
||||
@ -23,4 +24,4 @@ urlpatterns = [
|
||||
path('resource/<int:resource_id>/edit/', views.edit_resource, name='edit_resource'),
|
||||
path('resource/<int:resource_id>/delete/', views.delete_resource, name='delete_resource'),
|
||||
path('resource/<int:resource_id>/view/', views.view_resource, name='view_resource'),
|
||||
]
|
||||
]
|
||||
|
||||
@ -7,7 +7,8 @@ from django.contrib.admin.views.decorators import staff_member_required
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth import logout, login
|
||||
from django.urls import reverse
|
||||
from .models import Classroom, Subject, Teacher, Student, City, Resource
|
||||
from django.db.models import Q
|
||||
from .models import Classroom, Subject, Teacher, Student, City, Resource, Package
|
||||
from .forms import StudentRegistrationForm, TeacherProfileForm, ResourceForm
|
||||
from .wablas import send_whatsapp_message
|
||||
from .thawani import ThawaniClient
|
||||
@ -175,10 +176,20 @@ def profile(request):
|
||||
id__in=subscribed_subjects.values_list('id', flat=True)
|
||||
)
|
||||
|
||||
# Get active packages
|
||||
# Filter by student's classroom or global packages (classroom is None)
|
||||
packages = Package.objects.filter(is_active=True)
|
||||
if student_profile.classroom:
|
||||
packages = packages.filter(Q(classroom=student_profile.classroom) | Q(classroom__isnull=True))
|
||||
else:
|
||||
# If student has no classroom, show only global packages
|
||||
packages = packages.filter(classroom__isnull=True)
|
||||
|
||||
return render(request, 'core/student_dashboard.html', {
|
||||
'student_profile': student_profile,
|
||||
'subscribed_subjects': subscribed_subjects,
|
||||
'available_subjects': available_subjects
|
||||
'available_subjects': available_subjects,
|
||||
'packages': packages
|
||||
})
|
||||
|
||||
# Fallback (Superuser or Admin without profile)
|
||||
@ -237,6 +248,32 @@ def subscribe_subject(request, subject_id):
|
||||
print(f"Payment Error: {e}")
|
||||
return render(request, 'core/error.html', {'message': f'فشل بدء عملية الدفع: {str(e)}'})
|
||||
|
||||
@login_required
|
||||
def subscribe_package(request, package_id):
|
||||
try:
|
||||
student = request.user.student_profile
|
||||
except Student.DoesNotExist:
|
||||
return redirect('index')
|
||||
|
||||
package = get_object_or_404(Package, pk=package_id)
|
||||
|
||||
try:
|
||||
thawani = ThawaniClient()
|
||||
success_url = request.build_absolute_uri(reverse('payment_success')) + '?session_id={session_id}'
|
||||
cancel_url = request.build_absolute_uri(reverse('payment_cancel'))
|
||||
|
||||
session = thawani.create_checkout_session(package, request.user, success_url, cancel_url)
|
||||
|
||||
session_id = session.get('data', {}).get('session_id')
|
||||
if not session_id:
|
||||
return render(request, 'core/error.html', {'message': 'تعذر إنشاء جلسة الدفع.'})
|
||||
|
||||
return redirect(f"{thawani.checkout_base_url}/{session_id}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Payment Error: {e}")
|
||||
return render(request, 'core/error.html', {'message': f'فشل بدء عملية الدفع: {str(e)}'})
|
||||
|
||||
@login_required
|
||||
def payment_success(request):
|
||||
session_id = request.GET.get('session_id')
|
||||
@ -250,14 +287,26 @@ def payment_success(request):
|
||||
payment_status = data.get('payment_status')
|
||||
metadata = data.get('metadata', {})
|
||||
subject_id = metadata.get('subject_id')
|
||||
package_id = metadata.get('package_id')
|
||||
|
||||
if payment_status == 'paid' and subject_id:
|
||||
try:
|
||||
student = request.user.student_profile
|
||||
subject = get_object_or_404(Subject, pk=subject_id)
|
||||
student.subscribed_subjects.add(subject)
|
||||
except Exception:
|
||||
pass # Already handled or user mismatch?
|
||||
if payment_status == 'paid':
|
||||
student = request.user.student_profile
|
||||
|
||||
if subject_id:
|
||||
try:
|
||||
subject = get_object_or_404(Subject, pk=subject_id)
|
||||
student.subscribed_subjects.add(subject)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if package_id:
|
||||
try:
|
||||
package = get_object_or_404(Package, pk=package_id)
|
||||
for subject in package.subjects.all():
|
||||
student.subscribed_subjects.add(subject)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return redirect('profile')
|
||||
else:
|
||||
return render(request, 'core/error.html', {'message': f'لم تتم عملية الدفع بنجاح. الحالة: {payment_status}'})
|
||||
|
||||
BIN
media/resources/Voucher-_2.pdf
Normal file
BIN
media/resources/Voucher-_2.pdf
Normal file
Binary file not shown.
BIN
media/resources/بسم_الله_الرحمن_الرحيم.docx
Normal file
BIN
media/resources/بسم_الله_الرحمن_الرحيم.docx
Normal file
Binary file not shown.
39
test_regex.py
Normal file
39
test_regex.py
Normal file
@ -0,0 +1,39 @@
|
||||
import re
|
||||
|
||||
def get_youtube_id(url):
|
||||
"""Extracts the video ID from a YouTube URL."""
|
||||
if not url:
|
||||
return None
|
||||
|
||||
# regex for extracting youtube id
|
||||
# Supports:
|
||||
# - https://www.youtube.com/watch?v=VIDEO_ID
|
||||
# - https://youtu.be/VIDEO_ID
|
||||
# - https://www.youtube.com/embed/VIDEO_ID
|
||||
# - https://www.youtube.com/live/VIDEO_ID
|
||||
# - https://m.youtube.com/watch?v=VIDEO_ID
|
||||
|
||||
patterns = [
|
||||
r'(?:v=|\/)([0-9A-Za-z_-]{11}).*',
|
||||
r'(?:youtu\.be\/)([0-9A-Za-z_-]{11})',
|
||||
r'(?:embed\/)([0-9A-Za-z_-]{11})',
|
||||
r'(?:live\/)([0-9A-Za-z_-]{11})',
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, url)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return None
|
||||
|
||||
test_urls = [
|
||||
"https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
"https://youtu.be/dQw4w9WgXcQ",
|
||||
"https://www.youtube.com/embed/dQw4w9WgXcQ",
|
||||
"https://www.youtube.com/live/dQw4w9WgXcQ?feature=share",
|
||||
"https://m.youtube.com/watch?v=dQw4w9WgXcQ&list=RDdQw4w9WgXcQ",
|
||||
"invalid-url"
|
||||
]
|
||||
|
||||
for url in test_urls:
|
||||
print(f"URL: {url} -> ID: {get_youtube_id(url)}")
|
||||
29
test_regex_v2.py
Normal file
29
test_regex_v2.py
Normal file
@ -0,0 +1,29 @@
|
||||
import re
|
||||
|
||||
def get_youtube_id(url):
|
||||
if not url:
|
||||
return None
|
||||
patterns = [
|
||||
r'(?:v=|\/)([0-9A-Za-z_-]{11}).*',
|
||||
r'(?:youtu\.be\/)([0-9A-Za-z_-]{11})',
|
||||
r'(?:live\/)([0-9A-Za-z_-]{11})',
|
||||
r'(?:embed\/)([0-9A-Za-z_-]{11})',
|
||||
r'(?:shorts\/)([0-9A-Za-z_-]{11})', # Added shorts
|
||||
]
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, url)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return None
|
||||
|
||||
urls = [
|
||||
"https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
"https://youtu.be/dQw4w9WgXcQ",
|
||||
"https://www.youtube.com/embed/dQw4w9WgXcQ",
|
||||
"https://www.youtube.com/live/dQw4w9WgXcQ",
|
||||
"https://www.youtube.com/shorts/dQw4w9WgXcQ",
|
||||
"https://m.youtube.com/watch?v=dQw4w9WgXcQ&feature=youtu.be"
|
||||
]
|
||||
|
||||
for url in urls:
|
||||
print(f"{url} -> {get_youtube_id(url)}")
|
||||
43
test_regex_v3.py
Normal file
43
test_regex_v3.py
Normal file
@ -0,0 +1,43 @@
|
||||
import re
|
||||
|
||||
def get_youtube_id(url):
|
||||
"""Extracts the video ID from a YouTube URL."""
|
||||
if not url:
|
||||
return None
|
||||
|
||||
url = url.strip()
|
||||
|
||||
# Handle various formats:
|
||||
# https://youtu.be/VIDEO_ID
|
||||
# https://www.youtube.com/watch?v=VIDEO_ID
|
||||
# https://www.youtube.com/live/VIDEO_ID
|
||||
# https://www.youtube.com/embed/VIDEO_ID
|
||||
# https://www.youtube.com/shorts/VIDEO_ID
|
||||
patterns = [
|
||||
r'(?:v=|\/)([0-9A-Za-z_-]{11})(?:[?&]|$)',
|
||||
r'(?:youtu\.be\/)([0-9A-Za-z_-]{11})',
|
||||
r'(?:live\/)([0-9A-Za-z_-]{11})',
|
||||
r'(?:embed\/)([0-9A-Za-z_-]{11})',
|
||||
r'(?:shorts\/)([0-9A-Za-z_-]{11})',
|
||||
]
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, url)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return None
|
||||
|
||||
urls = [
|
||||
("https://www.youtube.com/watch?v=dQw4w9WgXcQ", "dQw4w9WgXcQ"),
|
||||
("https://youtu.be/dQw4w9WgXcQ", "dQw4w9WgXcQ"),
|
||||
("https://www.youtube.com/embed/dQw4w9WgXcQ", "dQw4w9WgXcQ"),
|
||||
("https://www.youtube.com/shorts/dQw4w9WgXcQ", "dQw4w9WgXcQ"),
|
||||
("https://www.youtube.com/live/dQw4w9WgXcQ", "dQw4w9WgXcQ"),
|
||||
(" https://www.youtube.com/watch?v=dQw4w9WgXcQ ", "dQw4w9WgXcQ"), # Whitespace
|
||||
("https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=1s", "dQw4w9WgXcQ"),
|
||||
("https://m.youtube.com/watch?v=dQw4w9WgXcQ", "dQw4w9WgXcQ"),
|
||||
]
|
||||
|
||||
print("Testing YouTube ID extraction...")
|
||||
for url, expected in urls:
|
||||
result = get_youtube_id(url)
|
||||
print(f"URL: {url.strip()} -> Expected: {expected}, Got: {result} -> {'PASS' if result == expected else 'FAIL'}")
|
||||
Loading…
x
Reference in New Issue
Block a user