40 KiB
Velody
خطة المعمارية وبيئة المشروع
Phase 1
هذه الوثيقة تغطي فقط:
- المعمارية الكاملة
- خطة التنفيذ
- هيكل المشروع
- استراتيجيات التخزين والمزامنة
- بيئة التطوير والتشغيل
ولا تبدأ بتنفيذ التطبيق الكامل.
0. قرارات تأسيسية
- النظام خاص جدًا لمالك واحد فقط.
- لا يوجد public multi-user auth في المرحلة الأولى.
- يوجد حد أدنى من الحماية التقنية بين الأجهزة والخادم عبر device install token أو shared secret مخزن في Keychain بدون بناء نظام حسابات.
- المصدر الحقيقي للمكتبة هو الخادم.
- تطبيق macOS هو مصدر الإدخال ingest source للملفات.
- تطبيق iPhone هو مستهلك للمكتبة مع تنزيل محلي كامل وتشغيل offline-first .
- الخلفية الموصى بها هي modular monolith باستخدام NestJS وليس microservices في البداية.
- التخزين الصوتي يكون على قرص VPS المحلي، وليس داخل PostgreSQL .
- المزامنة بين الأجهزة تبنى على append-only change log بدل إعادة تحميل كامل المكتبة كل مرة.
1. نظرة عامة على النظام
الهدف العملي
النظام المطلوب هو مكتبة موسيقى خاصة تعمل كمنظومة شخصية شبيهة بخفة Spotify ، لكن بدون تعقيد المستخدمين العامين أو إدارة الحسابات أو المشاركة العامة.
النتيجة المعمارية المطلوبة
- المستخدم يضع ملفات MP3 داخل مجلد مراقب على macOS .
- تطبيق macOS يلتقط التغييرات، ينتظر استقرار الملف، يستخرج البيانات الوصفية، ثم يضع الملف في طابور رفع دائم.
- الخادم يستقبل الملف بشكل قابل للاستئناف، يتحقق من سلامته، يحفظه على القرص، ويسجل بياناته الوصفية في قاعدة البيانات.
- الخادم يضيف حدثًا جديدًا إلى سجل التغييرات.
- تطبيق iPhone يقرأ التغييرات الجديدة، ينزل الملفات المطلوبة في الخلفية، يتحقق من سلامتها، ويحفظها محليًا للتشغيل بدون إنترنت.
التوصية الأساسية
ابدأ بـ:
- تطبيقين أصليين SwiftUI .
- حزم مشتركة Swift Package للمنطق المشترك.
- خادم Node.js + TypeScript + NestJS .
- قاعدة PostgreSQL .
- تخزين ملفات على قرص VPS .
- واجهات REST API بإصدار versioned API من البداية.
2. مخطط نصي للنظام
+----------------------+
| Watched Folder |
| on macOS |
+----------+-----------+
|
v
+----------------------+
| FSEvents Watcher |
| + Stability Check |
+----------+-----------+
|
v
+----------------------+
| Local Upload Queue |
| + Metadata Extract |
| + SHA-256 Hashing |
+----------+-----------+
|
v
+----------------------------------------+
| HTTPS REST API via Nginx |
| music.diyaa.de |
+----------------+-----------------------+
|
v
+----------------------------------------+
| NestJS Backend |
| - Upload Sessions |
| - Library Service |
| - Sync Events |
| - Validation / Metadata |
+-----------+----------------------------+
|
+-------------+-------------+
| |
v v
+-----------------------+ +---------------------------+
| PostgreSQL | | VPS File Storage |
| metadata + sync log | | audio/artwork/incoming |
+-----------+-----------+ +-------------+-------------+
| |
+---------------+---------------+
|
v
+--------------------------+
| Sync Changes Endpoint |
+------------+-------------+
|
v
+--------------------------+
| iPhone Sync Engine |
| BGTasks + URLSession |
+------------+-------------+
|
v
+--------------------------+
| Local Offline Store |
| + Core Data catalog |
| + local audio files |
+------------+-------------+
|
v
+--------------------------+
| AVPlayer / AVQueuePlayer |
| background playback |
+--------------------------+
3. هيكل المشروع الموصى به
velody/
apps/
apple/
Velody.xcworkspace
VelodyMac/
VelodyMacApp/
VelodyMacTests/
VelodyiPhone/
VelodyiPhoneApp/
VelodyiPhoneTests/
packages/
apple/
VelodyDomain/
VelodyNetworking/
VelodyPersistence/
VelodySync/
VelodyPlayback/
VelodyUtilities/
backend/
src/
main.ts
app.module.ts
modules/
health/
config/
devices/
library/
uploads/
metadata/
artwork/
sync/
storage/
common/
dto/
pipes/
filters/
interceptors/
utils/
infrastructure/
database/
filesystem/
logging/
prisma/
schema.prisma
migrations/
test/
integration/
e2e/
Dockerfile
package.json
infra/
docker/
compose.local.yml
compose.vps.yml
env/
backend.env.example
postgres.env.example
nginx/
music.diyaa.de.conf
scripts/
deploy.sh
backup-db.sh
backup-storage.sh
runtime/
local-dropbox/
storage/
logs/
docs/
PROJECT_ENVIRONMENT_ARCHITECTURE.md
لماذا هذا الهيكل مناسب
- يفصل بوضوح بين Apple apps و backend و infra .
- يسمح بمشاركة المنطق الحقيقي فقط بين macOS و iPhone بدل إجبار واجهات متطابقة.
- يبقي Docker وملفات التشغيل منفصلة عن كود التطبيق.
- يسمح بإضافة widgets أو watchOS أو CarPlay لاحقًا بدون إعادة تنظيم جذرية.
4. المعمارية الخلفية الموصى بها
الأسلوب العام
التوصية هي:
modular monolith
باستخدام:
NestJS
داخل حاوية واحدة في البداية.
هذا الخيار يعطيك:
- هيكلة واضحة
- قابلية اختبار أعلى
- سهولة إضافة وحدات مستقبلية
- بساطة تشغيلية أكبر من microservices
الوحدات الأساسية
1. HealthModule
المسؤولية:
- فحص الجاهزية
- فحص الاتصال بقاعدة البيانات
- فحص إمكانية الكتابة على التخزين
2. ConfigModule
المسؤولية:
- تحميل environment variables
- التحقق من القيم الإلزامية
- فصل إعدادات dev / staging / production
3. DevicesModule
المسؤولية:
- تسجيل الأجهزة
- حفظ نوع الجهاز وإصداره وآخر نشاط
- إدارة install token أو device secret
4. UploadsModule
المسؤولية:
- إنشاء جلسات رفع resumable upload sessions
- متابعة offset الحالي
- استقبال الأجزاء chunks
- إكمال الرفع أو رفضه
5. StorageModule
المسؤولية:
- التعامل مع نظام الملفات
- الكتابة إلى incoming ثم النقل إلى التخزين النهائي
- إرجاع storage keys
- عزل منطق التخزين بحيث يمكن لاحقًا استبداله بـ S3-compatible storage دون كسر طبقة الأعمال
6. MetadataModule
المسؤولية:
- استخراج بيانات MP3 بعد الرفع
- تطبيع الحقول normalization
- استخراج الغلاف artwork إن وجد
- التحقق من مدة الملف وصيغته وخصائصه التقنية
7. LibraryModule
المسؤولية:
- إنشاء أو تحديث السجل المنطقي للأغنية
- ربط السجل بالأصل الصوتي audio asset
- إدارة حالة الحذف المنطقي soft delete
- دعم الاستعلام عن المكتبة
8. ArtworkModule
المسؤولية:
- تخزين الغلاف إن وجد
- إعادة تقديمه للأجهزة
- دعم caching headers و ETag
9. SyncModule
المسؤولية:
- تسجيل أحداث التغيير
- تقديم bootstrap أولي للمكتبة
- تقديم incremental sync للأجهزة
ما الذي لا أنصح به الآن
- لا Redis في البداية.
- لا Kafka أو RabbitMQ .
- لا تقسيم إلى عدة خدمات منفصلة.
- لا GraphQL لأن المطلوب بسيط وواضح ويخدمه REST بشكل ممتاز.
5. تصميم قاعدة البيانات
المبدأ
افصل بين:
- الكيان المنطقي للأغنية track
- الملف الفعلي المخزن audio asset
- سجل التغييرات sync events
هذا يمنع ربط كل شيء مباشرة بالملف نفسه، ويسهل دعم إعادة الترميز، الغلاف، أو تعديلات metadata لاحقًا.
الجداول الأساسية
devices
الغرض:
تعريف كل تثبيت شرعي للتطبيقات.
أعمدة مقترحة:
idplatformdevice_nameapp_versioninstall_token_hashlast_seen_atcreated_atupdated_at
ملاحظات:
- لا يمثل مستخدمًا.
- يمثل جهازًا موثوقًا داخل المنظومة الخاصة.
tracks
الغرض:
التمثيل المنطقي للأغنية داخل المكتبة.
أعمدة مقترحة:
idprimary_audio_asset_idartwork_asset_idtitleartistalbumalbum_artistgenredisc_numbertrack_numberyearduration_msstatuscreated_atupdated_atdeleted_at
ملاحظات:
- هذا الجدول لا يخزن الملف نفسه.
- هذا هو الكيان الذي ستشير إليه playlists و favorites و playback history لاحقًا.
audio_assets
الغرض:
وصف الملف الصوتي الفعلي المخزن على الخادم.
أعمدة مقترحة:
idtrack_idsha256storage_keyoriginal_filenamemime_typefile_extensionfile_size_bytesbit_rate_kbpssample_rate_hzchannelsduration_mssource_device_idcreated_at
قيود مهمة:
sha256يجب أن يكون unique .storage_keyيجب أن يكون unique .
artwork_assets
الغرض:
تخزين الغلاف المستخرج إن وجد.
أعمدة مقترحة:
idsha256storage_keymime_typewidthheightfile_size_bytescreated_at
upload_sessions
الغرض:
تمكين الرفع القابل للاستئناف.
أعمدة مقترحة:
iddevice_idexpected_sha256expected_size_bytesreceived_bytestemp_storage_pathstatusexpires_atcreated_atupdated_at
library_events
الغرض:
تغذية المزامنة التزايدية incremental sync بين الخادم والأجهزة.
أعمدة مقترحة:
idمن نوعbigserialentity_typeentity_idactionpayload_versioncreated_at
فكرة الجدول:
- أي إنشاء أو تحديث أو حذف يضيف صفًا جديدًا.
- كل جهاز يحتفظ بآخر event id تمت معالجته.
- هذا أبسط وأمتن من إعادة جلب كامل المكتبة كل مرة.
device_sync_cursors
الغرض:
معرفة آخر نقطة مزامنة لكل جهاز.
أعمدة مقترحة:
device_idlast_event_idlast_full_sync_atupdated_at
علاقات الجداول
devices 1 --- n upload_sessions
devices 1 --- 1 device_sync_cursors
tracks 1 --- n audio_assets
tracks 0 --- 1 artwork_assets
tracks 1 --- n library_events
جداول مستقبلية بدون كسر التصميم
playlistsplaylist_itemsfavoritesplayback_positionsrecently_playeddevice_content_state
6. معمارية تخزين الملفات
مبدأ التخزين
لا تستخدم اسم الملف الأصلي كمفتاح تخزين فعلي.
استخدم:
content-addressed storage
يعتمد على:
SHA-256
تخطيط التخزين على
VPS
/srv/velody/
data/
incoming/
<upload-session-id>.part
quarantine/
<upload-session-id>/
library/
audio/
ab/
cd/
<sha256>.mp3
artwork/
ef/
12/
<sha256>.jpg
temp/
backups/
postgres/
storage-manifests/
لماذا التخزين بالهاش أفضل
- يمنع مشاكل تكرار الأسماء.
- يمنع مشاكل الأحرف الغريبة في أسماء الملفات.
- يجعل التحقق من السلامة أسهل.
- يسهل التبديل لاحقًا إلى تخزين سحابي.
- يحسن تنظيم المجلدات عبر توزيع الملفات على مستويات فرعية.
استراتيجية التسمية
- اسم الملف النهائي يعتمد على SHA-256 .
- الامتداد يعتمد على النوع الفعلي المكتشف، وليس على الاسم المرسل فقط.
- الاسم الأصلي يحفظ في قاعدة البيانات لأغراض العرض أو التتبع فقط.
استراتيجية الكاش
على الخادم
- ملفات الصوت النهائية لا تعتبر cache بل هي أصل دائم.
- الغلاف يمكن تقديمه مع
ETagوCache-Controlمناسب.
على
macOS
- مجلد محلي لطابور الرفع
- قاعدة بيانات محلية لحالة الملفات
- ملفات مؤقتة للتحليل فقط
على
iPhone
- ملفات الصوت تحمل إلى
Application Support - ملفات الغلاف تحفظ في مسار منفصل
- الملفات المحلية تعامل كمكتبة offline library وليست مجرد streaming cache
استراتيجية العمل بدون إنترنت
- التشغيل يجب أن يتم دائمًا من الملف المحلي المتحقق من سلامته.
- الواجهة تعرض آخر مكتبة معروفة محليًا حتى عند انعدام الشبكة.
- المزامنة لاحقًا تحاول فقط جلب التغييرات الجديدة بعد عودة الاتصال.
7. معمارية المزامنة
أ. دورة رفع الملف من
macOS
- يصل حدث تغيير من مجلد المراقبة.
- يدخل الملف إلى مرحلة debounce وفحص الاستقرار.
- إذا بقي الحجم ووقت التعديل ثابتين لعدة ثوانٍ، يعتبر جاهزًا.
- يستخرج التطبيق بيانات أولية محليًا لتحسين تجربة المستخدم وسرعة التصنيف.
- يحسب SHA-256 عبر streaming وليس تحميل الملف كاملًا في الذاكرة.
- يستعلم عن حالة الملف عبر
POST /api/v1/uploads/prepare. - إذا كان الملف موجودًا مسبقًا بنفس الهاش: لا يعاد رفعه، بل يحدث الربط أو الحالة فقط.
- إذا لم يكن موجودًا: ينشئ الخادم upload session ويعيد upload id و next offset .
- يرفع العميل الملف على أجزاء متسلسلة.
- عند اكتمال الرفع: يطلب العميل finalize .
- الخادم يعيد حساب الهاش، ويتحقق من الحجم والصيغة.
- إذا نجح التحقق:
ينقل الملف من
incomingإلىlibrary/audioداخل عملية نهائية آمنة. - يحدث قاعدة البيانات داخل معاملة transaction واحدة.
- يضاف حدث جديد إلى
library_events.
ب. دورة تنزيل الملف إلى
iPhone
- التطبيق يحتفظ بآخر
event_idتمت معالجته. - عند فتح التطبيق أو تشغيل
background task
يطلب:
GET /api/v1/sync/changes?after=<event_id> - يحصل على: أغاني جديدة، تحديثات، وحذوفات.
- ينشئ التطبيق مهام تنزيل عبر background URLSession .
- ينزل الملف إلى مسار مؤقت.
- بعد الاكتمال يتحقق من: الحجم، الهاش، وربما ETag .
- ينقل الملف نقلاً ذريًا atomic move إلى موقعه النهائي.
- يحدث السجل المحلي وقيمة sync cursor .
- يصبح الملف قابلًا للتشغيل بالكامل بدون اتصال.
ج. استراتيجية إعادة المحاولة
في
macOS
- خزّن طابور الرفع في قاعدة محلية دائمة.
- استخدم exponential backoff with jitter .
- مثال عملي: 1 دقيقة، 5 دقائق، 15 دقيقة، ساعة، 6 ساعات.
- أخطاء 5xx و network errors تعاد تلقائيًا.
- أخطاء 4xx تعامل غالبًا كأخطاء دائمة وتظهر للمستخدم.
في
iPhone
- التنزيلات الكبيرة تتم عبر background session لتفادي توقفها عند انتقال التطبيق للخلفية.
- إذا انقطع التنزيل، يعتمد الاستئناف على دعم الخادم لطلبات
RangeوثباتETag.
د. معالجة التعارضات
في Phase 1 لا يوجد تعارض متعدد المستخدمين عمليًا، لذلك نستخدم قواعد بسيطة وآمنة:
قاعدة الحقيقة
- الخادم هو المصدر النهائي للمكتبة.
- تطبيق iPhone لا يكتب إلى المكتبة في هذه المرحلة.
الملفات المكررة
- نفس
sha256يعني نفس الأصل الصوتي. - لا يعاد رفع الملف ولا تنشأ نسخة جديدة افتراضيًا.
نفس المسار المحلي مع ملف جديد
إذا كان تطبيق macOS يربط ذلك المسار سابقًا مع track معين ثم تغيرت محتوياته:
- يرفع الأصل الجديد
- ثم يرسل
existingTrackIdفي مرحلة الإنهاء - فيحدث السجل الحالي بدل إنشاء سجل جديد
هذا يحافظ على تحديث الأغنية نفسها عند تعديل الملف أو إعادة تصديره من نفس المصدر المحلي.
الحذف
- يفضل في البداية استخدام soft delete على الخادم.
- يرسل حدث حذف إلى الأجهزة.
- تطبيق iPhone يزيل الملف المحلي عندما لا يكون قيد التشغيل النشط.
هـ. كشف التكرار
المرحلة الأولى:
- المفتاح الأساسي هو full-file SHA-256 .
ترقية مستقبلية اختيارية:
- إضافة audio fingerprint لتوحيد الملفات المعاد وسمها re-tagged files حتى لو تغيرت ID3 tags .
8. استراتيجية مراقبة المجلد في
macOS
الخيار الأفضل
استخدم:
FSEvents
عبر:
CoreServices
لماذا هذا هو الخيار الصحيح
- مراقبة مجلدات بشكل recursive وبكلفة منخفضة.
- مناسب للمجلدات التي تحتوي عددًا كبيرًا من الملفات.
- أفضل من DispatchSource عندما يكون المطلوب مراقبة شجرة مجلدات كاملة.
الاستراتيجية العملية
- يختار المستخدم مجلد المكتبة عبر
NSOpenPanel. - يحفظ التطبيق إذن الوصول كمؤشر دائم:
security-scoped bookmarkإذا كان التطبيق sandboxed أو تريد أن يبقى جاهزًا للتوزيع الرسمي. - يبدأ
FSEventStreamعلى جذر المجلد. - الأحداث لا تؤدي مباشرة إلى رفع الملف، بل إلى إعادة فحص ذكية للمسار المتأثر.
- لا يعالج التطبيق الملف قبل التأكد من استقراره.
- عند تشغيل التطبيق أول مرة أو بعد انقطاع طويل، ينفذ full reconciliation scan للمجلد كاملًا.
اعتبارات الاعتمادية
- تعامل مع حالات event coalescing و dropped events بإعادة مسح المسارات المتأثرة.
- إذا ظهر علم مثل:
MustScanSubDirsأو تغير جذر المجلد، نفذ مسحًا كاملًا. - تجاهل الملفات المخفية والمؤقتة مثل:
.partو.tmp. - اعتمد على قاعدة محلية تسجل آخر hash / mtime / size معروف لكل ملف.
اعتبارات الأداء
- لا تحسب الهاش لكل الملفات في كل حدث.
- استخدم مرحلتين:
فحص سريع
mtime + sizeثم hashing عند الحاجة فقط. - اجعل الرفع منفصلًا عن خيط watcher نفسه.
- حد أقصى لعدد عمليات الرفع المتزامنة.
9. استراتيجية التخزين والعمل بدون إنترنت في
iPhone
التخزين المحلي
الموصى به:
- قاعدة بيانات محلية Core Data للكتالوج والحالة
- ملفات الصوت داخل
Application Support - ملفات الغلاف داخل مسار منفصل
لماذا
Core Data هنا
- ناضج جدًا
- يدعم migrations بوضوح
- يعمل جيدًا مع مهام الخلفية
- مناسب أكثر من حلول أخف عندما تكون الأولوية للاستقرار طويل الأمد
أفضل الممارسات
- لا تحفظ الملفات في
Documentsإلا إذا أردت إظهارها للمستخدم كملفات. - استبعد ملفات الموسيقى من النسخ الاحتياطي السحابي إذا كانت قابلة لإعادة التنزيل:
isExcludedFromBackup. - لا تشغل من الملف المؤقت.
- شغّل فقط من الملف النهائي المتحقق من سلامته.
مزامنة الخلفية
استخدم:
BGAppRefreshTaskلفحص تغييرات خفيفةBGProcessingTaskلتنزيلات أكبر أو إعادة التحقق الدوريURLSessionConfiguration.background(withIdentifier:)للتنزيلات الفعلية
معمارية التشغيل الصوتي
استخدم:
AVPlayerللتشغيل الفرديAVQueuePlayerللطوابير المستقبليةAVAudioSessionبفئة.playbackMediaPlayerلدعم شاشة القفل وأوامر الوسائط
سلوك التشغيل بدون إنترنت
- بمجرد اكتمال تنزيل الملف والتحقق منه، يصبح التشغيل مستقلًا عن الشبكة تمامًا.
- الاحتفاظ بمسار الملف المحلي في السجل المحلي.
- الاحتفاظ بآخر موضع تشغيل محليًا تمهيدًا لمزامنته لاحقًا.
10. معمارية
API
الاستراتيجية العامة
- استخدم URI versioning من البداية.
- المسار الأساسي:
/api/v1 - لا تخلط بين مسارات الإدارة، الرفع، والمزامنة.
بنية المسارات المقترحة
صحة النظام
GET /api/v1/health
الأجهزة
POST /api/v1/devices/registerPOST /api/v1/devices/heartbeat
الرفع
POST /api/v1/uploads/prepareGET /api/v1/uploads/:uploadIdPATCH /api/v1/uploads/:uploadIdPOST /api/v1/uploads/:uploadId/finalize
المكتبة
GET /api/v1/tracksGET /api/v1/tracks/:trackIdGET /api/v1/tracks/:trackId/fileGET /api/v1/artwork/:artworkId
المزامنة
GET /api/v1/sync/bootstrapGET /api/v1/sync/changes
استراتيجية الرفع
لا أوصي في هذه المنظومة الخاصة بأن تبدأ بـ multipart single-shot upload فقط، لأن شرط الاستئناف مهم.
الأفضل:
- إنشاء جلسة رفع
- رفع متسلسل على أجزاء
- استعلام عن current offset
- إنهاء نهائي مع تحقق
استراتيجية metadata
- التطبيق على macOS يمكنه إرسال metadata أولية.
- الخادم يجب أن يعيد استخراجها أو يتحقق منها بعد اكتمال الرفع.
- metadata النهائية المعتمدة يجب أن تكون من الخادم.
استجابة المزامنة
يجب أن تعيد واجهة المزامنة:
- قائمة الأحداث الجديدة
- السجلات الكاملة المتأثرة
- الحذوفات كـ tombstones
- آخر
event_idجديد ليحفظه العميل
التوثيق
استخدم:
OpenAPI / Swagger
من البداية لتثبيت العقد API contract بين التطبيقات والخادم.
11. معمارية
Docker
محليًا
الخدمات المطلوبة:
apipostgresnginxاختياري للمطابقة مع الإنتاج
في
VPS
التوصية العملية:
- شغّل
apiوpostgresعبر Docker Compose . - شغّل Nginx على المضيف host مباشرة لسهولة إدارة TLS و Let's Encrypt .
هذا أكثر بساطة تشغيلية من حشر كل شيء داخل الحاويات في هذه المرحلة.
الحاويات
api
- صورة Node.js إنتاجية
- يربط مجلد التخزين الدائم
- يستمع داخليًا على منفذ خاص مثل
3000
postgres
- حاوية PostgreSQL
- بدون تعريض المنفذ للعالم الخارجي
- فقط على الشبكة الداخلية
الأحجام
volumes
محليًا
- حجم أو bind mount لبيانات PostgreSQL
- مجلد محلي لاختبار تخزين MP3
إنتاجيًا
postgres_data- ربط:
/srv/velody/dataإلى داخل الحاوية
الشبكات
- شبكة داخلية مثل:
velody_internal - قاعدة البيانات غير مكشوفة خارجيًا
- واجهة API لا تتعرض مباشرة للإنترنت
- المضيف فقط هو من يمرر الطلبات عبر Nginx
استراتيجية النشر على
VPS
- بناء صورة الخادم
- رفع الصورة أو بناؤها على الخادم
- تشغيل
docker compose up -d - تمرير
Nginx
إلى
127.0.0.1:<api-port> - تفعيل HTTPS عبر Let's Encrypt
ما الذي يجب إضافته تشغيليًا
- نسخة احتياطية لقاعدة البيانات
- نسخة احتياطية لتخزين الملفات
- فحص healthcheck دوري
- سجلات منظمة structured logs
12. المكتبات والأدوات الموصى بها
في تطبيقات
SwiftUI
الواجهة والحالة
SwiftUIObservationasync/await
الشبكات
URLSessionURLSessionConfiguration.background
التخزين المحلي
Core Data
الصوت
AVFoundationAVPlayerAVQueuePlayerMediaPlayer
مهام الخلفية
BackgroundTasks
مراقبة المجلد في
macOS
FSEventsعبرCoreServices
استخراج metadata والغلاف في
Apple side
AVFoundationAVAsset.commonMetadata
في الخلفية
Backend
الإطار الأساسي
NestJS
الوصول إلى قاعدة البيانات
Prisma ORM
التحقق من الطلبات
ValidationPipeclass-validator
رفع الملفات
@nestjs/platform-expressmulterمن خلال تكامل NestJS
التحقق من نوع الملف
file-type
استخراج
MP3 metadata
music-metadata
التشفير والتحقق
- مكتبة
cryptoالمدمجة في Node.js
التوثيق
@nestjs/swagger
أدوات الاختبار
XCTestلتطبيقات AppleJestأوVitestللخلفية- اختبارات e2e لواجهات API
13. اعتبارات الأمان
حتى بدون نظام حسابات، لا ينبغي ترك النظام مكشوفًا بلا حماية.
الحد الأدنى المطلوب
HTTPS only- عدم كشف PostgreSQL للعالم الخارجي
- عدم كشف منفذ الخادم مباشرة إن أمكن
- السماح بالوصول عبر Nginx فقط
حماية الوصول
- استخدم install token أو shared secret لكل تطبيق مثبت
- خزنه داخل Keychain على Apple platforms
- خزن على الخادم فقط hash للرمز وليس الرمز الخام
حماية الرفع
- تحقق من الحجم المتوقع
- تحقق من SHA-256
- تحقق من النوع الفعلي magic bytes وليس MIME المعلن فقط
- ارفض أي ملف ليس MP3 في المرحلة الأولى
- انقل الملفات المشبوهة إلى quarantine
حماية الملفات
- لا تستخدم المسارات المرسلة من العميل مباشرة على نظام الملفات
- لا تقدم الملفات من أسماء المستخدم الأصلية
- لا تسمح path traversal
حماية البيانات
- استخدم مستخدم قاعدة بيانات بصلاحيات محددة
- افصل أسرار البيئة عن المستودع
- فعّل نسخًا احتياطية دورية
- راقب امتلاء القرص لأن التخزين الصوتي هو نقطة الفشل الأكثر واقعية
14. اعتبارات القابلية للتوسع
ما الذي يجعل هذا التصميم قابلًا للنمو
1. فصل
track عن asset
يسمح لاحقًا بإضافة:
- صيغ متعددة
- نسخ منخفضة الجودة
- نسخ lossless
- معاينات
2. وجود
library_events
يسمح لاحقًا بـ:
- مزامنة أسرع
- عدة أجهزة
- متابعة continue listening
- مزامنة موجهة بالتغييرات بدل الفحص الكامل
3. طبقة
StorageModule
تسمح لاحقًا بنقل التخزين من قرص VPS إلى تخزين كائني object storage بدون كسر API أو منطق الأعمال.
4. طبقة
DevicesModule
تعطيك طريقًا نظيفًا لإضافة:
- أكثر من iPhone
- أكثر من Mac
- مزامنة متعددة الأجهزة
5. بنية
NestJS modules
تسمح بإضافة:
playlistsfavoritessearchplayback historylyrics
كوحدات مستقلة نسبيًا.
ما الذي لا يحتاج توسعة مبكرة
- لا تحتاج CDN الآن.
- لا تحتاج message broker الآن.
- لا تحتاج sharding أو distributed workers الآن.
15. خارطة طريق تنفيذية على مراحل
Phase 1
الهدف:
تثبيت المعمارية، البيئة، العقود، والهيكل.
يشمل:
- إنشاء المستودع وهيكله
- تجهيز Xcode workspace
- تجهيز NestJS الأساسي
- تجهيز Docker Compose
- تصميم قاعدة البيانات
- تعريف API contracts
- توثيق كامل للمشروع
الناتج:
مرجع معماري واضح وقابل للتنفيذ.
Phase 2
الهدف:
بناء الأساس الخلفي القابل للاختبار.
يشمل:
- إنشاء الخادم NestJS
- ربط PostgreSQL
- بناء Prisma schema
- إنشاء وحدات:
health,devices,uploads,storage,library,sync - تنفيذ رفع قابل للاستئناف
- حفظ ملفات MP3 على القرص
- بناء sync events
الناتج:
خادم عامل محليًا مع مسار رفع وتخزين حقيقي.
Phase 3
الهدف:
بناء تطبيق macOS كمصدر إدخال موثوق.
يشمل:
- اختيار مجلد المراقبة
- حفظ security-scoped bookmark
- بناء FSEvents watcher
- بناء طابور رفع دائم
- استخراج metadata والغلاف
- ربط الملف بـ existingTrackId عند التعديل من نفس المصدر المحلي
الناتج:
رفع تلقائي مستقر من macOS إلى الخادم.
Phase 4
الهدف:
بناء تطبيق iPhone بمكتبة محلية وتشغيل بدون إنترنت.
يشمل:
- شاشة مكتبة محلية
- مزامنة أولية bootstrap sync
- مزامنة تزايدية
- تنزيلات خلفية
- تخزين محلي دائم
- تشغيل AVPlayer في الخلفية
الناتج:
تطبيق iPhone يعمل كمشغل موسيقى محلي متصل بالخادم عند الحاجة فقط.
Phase 5
الهدف:
التقسية التشغيلية hardening .
يشمل:
- تحسين الاستئناف
- تحسين إعادة المحاولة
- تسجيلات منظمة
- مراقبة السعة
- نسخ احتياطية
- إدارة الحذف والمزامنة الدقيقة
- اختبارات e2e
Phase 6
الهدف:
إضافة ميزات المنتج الأساسية.
يشمل:
playlistsfavoritessearchshuffle / repeatrecently playedcontinue listening- مزامنة مواضع التشغيل
Phase 7
الهدف:
توسعة المنظومة بدون إعادة كتابة الأساس.
يشمل:
CarPlayApple Watch- أكثر من جهاز لكل نوع
- مكتبات مشتركة مستقبلًا
- نظام صلاحيات حقيقي إذا لزم لاحقًا
16. بيئة التطوير والتشغيل المقترحة
محليًا
تطبيقات
Apple
XcodeSwift 5.9+SwiftUIiOS Simulator
الخلفية
Node.js 20+ويفضل اعتماد إصدار LTS موحد في الفريقnpmأوpnpmDocker DesktopPostgreSQLداخل Docker
مسارات محلية مفيدة
runtime/local-dropbox/ -> مجلد إسقاط MP3 للتجربة
runtime/storage/ -> تخزين ملفات backend محليًا
runtime/logs/ -> سجلات التطوير
إنتاجيًا على
VPS
Debian 12Docker EngineDocker Compose pluginPostgreSQL- حاوية backend
- تخزين محلي على القرص
NginxLet's Encrypt- نطاق مثل:
music.diyaa.de
متغيرات بيئة أساسية
NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://...
STORAGE_ROOT=/srv/velody/data
PUBLIC_BASE_URL=https://music.diyaa.de
DEVICE_BOOTSTRAP_SECRET=...
MAX_UPLOAD_SIZE_BYTES=...
17. توصية ختامية
إذا كان الهدف هو بناء منظومة شخصية قوية، بسيطة، وتعيش طويلًا بدون إعادة كتابة لاحقًا، فهذه هي أفضل نقطة انطلاق عملية:
SwiftUInative appsNestJSmodular monolithPostgreSQLللبيانات الوصفية- تخزين ملفات على قرص
VPS - مزامنة مبنية على
library_events - رفع قابل للاستئناف من
macOS - تنزيلات خلفية وتخزين محلي كامل على
iPhone
هذه البنية ليست مبالغًا فيها، لكنها أيضًا ليست مؤقتة أو هشة. هي مناسبة جدًا لمنتج خاص يشبه Spotify من حيث الإحساس، لكن بدون تعقيد بنية خدمة عامة.
18. مراجع رسمية مفيدة
- Apple
FSEventshttps://developer.apple.com/library/archive/documentation/Darwin/Conceptual/FSEvents_ProgGuide/UsingtheFSEventsFramework/UsingtheFSEventsFramework.html - Apple
BackgroundTaskshttps://developer.apple.com/documentation/uikit/using-background-tasks-to-update-your-app - Apple
URLSession background transfershttps://developer.apple.com/documentation/foundation/urlsessionconfiguration/1407496-background - Apple
AVAudioSessionhttps://developer.apple.com/documentation/avfaudio/avaudiosession - Apple
AVPlayerhttps://developer.apple.com/documentation/avfoundation/avplayer - Apple
AVQueuePlayerhttps://developer.apple.com/documentation/avfoundation/avqueueplayer - Apple
Core Datahttps://developer.apple.com/documentation/coredata - Apple
macOS App Sandbox and security-scoped bookmarkshttps://developer.apple.com/documentation/security/app_sandbox/accessing_files_from_the_macos_app_sandbox - NestJS
Moduleshttps://docs.nestjs.com/modules - NestJS
Validationhttps://docs.nestjs.com/techniques/validation - NestJS
File uploadhttps://docs.nestjs.com/techniques/file-upload - NestJS
Versioninghttps://docs.nestjs.com/techniques/versioning - NestJS
OpenAPIhttps://docs.nestjs.com/openapi/introduction - Prisma https://www.prisma.io/docs
- Docker Compose https://docs.docker.com/compose/
- Docker networks https://docs.docker.com/compose/how-tos/networking/
- Docker volumes https://docs.docker.com/reference/compose-file/volumes/
- PostgreSQL https://www.postgresql.org/docs/
- PostgreSQL
pgcryptohttps://www.postgresql.org/docs/16/pgcrypto.html music-metadatahttps://github.com/Borewit/music-metadatafile-typehttps://github.com/sindresorhus/file-type