From fa3a047519a70fdbc6e3ce7e0464c52a652952cb Mon Sep 17 00:00:00 2001 From: abvgeej Date: Sun, 24 Feb 2019 21:45:56 +0000 Subject: [PATCH 001/138] Translated using Weblate (Russian) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-ru/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 32d57db8b..fa388df8b 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -49,7 +49,7 @@ Путь загрузки аудио Папка для хранения загруженных аудио Введите путь к папке для загрузки аудио - Начните с поиска + Коснитесь поиска, чтобы начать Подождите… Файл уже существует Потоки @@ -141,7 +141,7 @@ Лучшее разрешение Запрос reCAPTCHA Запрошен ввод reCAPTCHA - Высокие разрешения + Показывать высокие разрешения NewPipe в окне О NewPipe Настройки From b9187445e0a1b55b0e3c45e994bdac91d9bd50d6 Mon Sep 17 00:00:00 2001 From: Javi Date: Wed, 27 Feb 2019 20:19:36 +0000 Subject: [PATCH 002/138] Translated using Weblate (Spanish) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-es/strings.xml | 188 ++++++++----------------- 1 file changed, 60 insertions(+), 128 deletions(-) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 60257c508..47748195d 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -29,25 +29,20 @@ URL no soportada Usar reproductor de vídeo externo Usar reproductor de audio externo - Tema Oscuro Claro - Apariencia Otros Reproduciendo en segundo plano Contenido no disponible Usar Tor (Experimental) Forzar la descarga a través de Tor para una mayor privacidad (transmisión de vídeos aún no compatible). - No se puede crear la carpeta de descarga \'%1$s\' Carpeta de descarga creada \'%1$s\' Los audios descargados se almacenan aquí Introducir ruta de descarga para archivos de audio - Bloqueado por GEMA - Carpeta de descarga de audio Vídeo y audio Reproducir @@ -64,12 +59,9 @@ No me gusta Miniatura del avatar del usuario Las transmisiones en vivo aún no están soportadas - - Contenido Contenido restringido por edad - Mostrar vídeo restringido por edad. Permitir este tipo de material es posible desde \"Ajustes\". - + Mostrar vídeo restringido por edad. Se puede permitir este tipo de material desde Ajustes. Toque en buscar para empezar Reproducción automática Reproducir un vídeo cuando NewPipe es llamado desde otra app @@ -77,7 +69,6 @@ Descargas Descargas Reportar error - No se pudo analizar el sitio web completamente No se pudo configurar el menú de descarga No se pudo obtener ninguna transmisión @@ -89,26 +80,19 @@ Qué ha ocurrido: Su comentario (en Inglés): Detalles: - - Reportar un error Reporte de usuario - Vídeo - Audio Reintentar Permiso de acceso al almacenamiento denegado - Iniciar Pausar Reproducir Eliminar Checksum - Nueva misión OK - Nombre del archivo Hilos Error @@ -120,60 +104,45 @@ Espere, por favor … Copiado al portapapeles Por favor, seleccione un directorio de descarga disponible - No se pudo cargar la imagen La interfaz de la app dejó de funcionar Lo sucedido:\\nPetición:\\nIdioma del contenido:\\nServicio:\\nHora GMT:\\nPaquete:\\nVersión:\\nVersión del SO: - Negro - Todo Canal Después - K M MM - reCAPTCHA Abrir en modo popup Este permiso es necesario para abrir en modo popup - Reto reCAPTCHA Reto reCAPTCHA requerido - Modo popup de NewPipe - Reproduciendo en modo popup Formato de vídeo por defecto Desactivado - Mostrar resoluciones más altas Solo algunos dispositivos soportan reproducción de vídeos en 2K/4K Resolución por defecto del popup Segundo plano Popup - Filtro Actualizar Limpiar - Recordar tamaño y posición del popup Recordar el último tamaño y posición del popup - Popup Redimensionando - Elimina el audio en algunas resoluciones Controles de gestos del reproductor Usar gestos para controlar el brillo y volumen del reproductor Sugerencias de búsqueda Mostrar sugerencias cuando esté buscando - -Mejor resolución - + Mejor resolución Acerca de NewPipe Ajustes Acerca de @@ -190,28 +159,22 @@ abrir en modo popup Si tienes ideas de; traducción, cambios de diseño, limpieza de código o cambios de código realmente fuertes—la ayuda siempre es bienvenida. Cuanto más se hace, mejor se pone! Leer licencia Contribuir -Suscribirse + Suscribirse Suscrito Canal no suscrito No se pudo cambiar la suscripción No se pudo actualizar la suscripción - Principal Suscripciones - Qué hay de nuevo - Reanudar al enfocar Continuar reproduciendo después de las interrupciones (ej. llamadas telefónicas) - Descargar Caracteres permitidos en los nombres de archivo Los caracteres no válidos se reemplazan por este valor Carácter de reemplazo - Letras y dígitos La mayoría de caracteres especiales - Historial de búsqueda Almacenar búsquedas localmente Historial y caché @@ -223,40 +186,33 @@ abrir en modo popup Historial El historial está vacío Historial borrado - -Notificación de NewPipe + Notificación de NewPipe Notificaciones para NewPipe en segundo plano y reproductores popup - Reproductor Funcionamiento Historial y caché Lista de reproducción Deshacer - No hay resultados Nada más que grillos - Sin suscriptores - %s suscriptor - %s suscriptores - - + %s suscriptor + %s suscriptores + Sin reproducciones - %s reproducción - %s reproducciones - - + %s reproducción + %s reproducciones + Sin vídeos - %s vídeo - %s vídeos - - + Vídeo + Vídeos + Elemento eliminado -¿Desea eliminar este elemento del historial de búsqueda? -Contenido de la página principal + ¿Desea eliminar este elemento del historial de búsqueda? + Contenido de la página principal Página en blanco Página del kiosco Página de suscripción @@ -265,32 +221,28 @@ abrir en modo popup Seleccione un canal No hay canales suscritos todavía Seleccione un kiosco - Kiosco Tendencias Top 50 -Mostrar sugerencia cuando se presiona el botón de segundo plano o popup en la página de detalles del vídeo + Mostrar sugerencia cuando se presiona el botón de segundo plano o popup en la página de detalles del vídeo En cola en el reproductor de fondo En cola en el reproductor popup Reproducir todo - No se pudo reproducir este stream Se produjo un error irrecuperable del reproductor Recuperándose del error del reproductor - Reproductor en segundo plano Reproductor popup Quitar Detalles Ajustes de audio [Desconocido] - Poner en cola de segundo plano Poner en cola de popup Comenzar a reproducir aquí Comenzar aquí en segundo plano Comenzar aquí en popup -Mostrar consejo \"Mantener para poner en la cola\" + Mostrar consejo \"Mantener para poner en la cola\" Nuevo y popular Mantener para poner en la cola Donar @@ -303,27 +255,23 @@ abrir en modo popup Cambiar a segundo plano Cambiar a popup Cambiar a principal - Servicio Abrir cajón Cerrar cajón No se ha encontrado ningún reproductor de vídeo (puede instalar VLC para reproducirlo). Siempre Sólo una vez - Los reproductores externos no soportan este tipo de enlaces URL no válida No se encontraron transmisiones de vídeo No se encontraron transmisiones de audio - Reproductor de vídeo Reproductor de fondo Reproductor de popup Preguntar siempre - Obteniendo información… Cargando contenido solicitado -Importar base de datos + Importar base de datos Exportar base de datos Reemplazará su historial actual y sus suscripciones Exportar historial, suscripciones y listas de reproducción @@ -332,90 +280,67 @@ abrir en modo popup Archivo ZIP no válido ADVERTENCIA: no se pudieron importar todos los archivos. Esto reemplazará su configuración actual. - Descargar archivo stream Mostrar info - "Listas de reproducción en marcadores " - Añadir a - Arrastrar para reordenar - Crear Eliminar uno Eliminar todos Descartar Renombrar - ¿Desea eliminar este elemento del historial de reproducciones? ¿Seguro que desea eliminar todos los elementos del historial? Última reproducción Más reproducido - Preguntar siempre - Nueva lista de reproducción Eliminar Renombrar Nombre Añadir a la lista de reproducción Definir como miniatura de lista de reproducción - Marcar lista de reproducción Eliminar marcador - ¿Borrar esta lista de reproducción\? Lista de reproducción creada Añadido a la lista de reproducción Miniatura de lista de reproducción cambiada. No se pudo eliminar la lista de reproducción. - Algo aparecerá aquí pronto ;D - - Sin subtítulos - Ajustar Rellenar Zoom - -Depuración + Depuración Auto generados Activar LeakCanary La monitorización de fugas de memoria puede causar que la app no responda cuando hay Heap Dump - Reportar errores fuera del ciclo de duración Forzar reporte de excepciones no entregables de RX fuera del fragmento o del ciclo de actividad después del descarte - -Usar búsqueda rápida inexacta + Usar búsqueda rápida inexacta La búsqueda inexacta permite al reproductor buscar posiciones más rápido con menor precisión Auto-encolar la siguiente transmisión Auto-añadir un vídeo relacionado al reproducir el último vídeo en una cola no repetitiva. DIRECTO SINCRONIZAR -Archivo - + Archivo No existe el directorio No existe la fuente del archivo/contenido El archivo no existe o insuficientes permisos para leerlo o escribir en él El nombre del archivo no puede estar vacío Ocurrió un error: %1$s - Importar/exportar Importar Importar desde Exportar a - Importando… Exportando… - Importar archivo Exportación anterior - No se pudo importar suscripciones No se pudo exportar suscripciones - Importe suscripciones de YouTube descargando el archivo de exportación: \n \n1. Vaya a esta URL: %1$s @@ -428,12 +353,11 @@ abrir en modo popup \n3. Inicie sesión cuando se le pida \n4. Copie la URL del perfil a la que fue redireccionado. suID, soundcloud.com/suID - Tenga en cuenta que esta operación puede ser costosa para la red. \n \n¿Desea continuar? -Cargar Miniaturas - Cuando está desactivado no se cargan miniaturas, economizando el uso de datos y memoria. Los cambios borrarán tanto la caché de imágenes en la memoria como en el disco. + Cargar miniaturas + Desactívalo para evitar la carga de miniaturas, ahorrando datos y uso de memoria. Los cambios borrarán tanto la caché de imágenes en la memoria como en el disco. Caché de imagen limpiado Metadatos eliminados del caché Eliminar todos los datos de la página web en caché @@ -442,16 +366,12 @@ abrir en modo popup Tiempo Tono Desenganchar (puede causar distorsión) -No hay streams disponibles para descargar - + No hay streams disponibles para descargar Acción \'abrir\' preferida Acción por defecto al abrir contenido — %s - No hay ninguna app instalada para reproducir este archivo - Subtítulos Modificar la escala de texto de los subtítulos y los estilos de fondo del reproductor. Requiere el reinicio de la app para que surta efecto. - Borrar historial de reproducciones Elimina el historial de las transmisiones reproducidas ¿Eliminar todo el historial de reproducciones\? @@ -461,68 +381,54 @@ abrir en modo popup ¿Eliminar todo el historial de búsqueda\? Historial de búsquedas eliminado. 1 elemento eliminado. - NewPipe es software libre copyleft: puedes usarlo, estudiarlo, compartirlo y mejorarlo a tu antojo. Específicamente, puedes redistribuirlo y/o modificarlo bajo los términos de la Licencia Pública General GNU publicada por la Free Software Foundation, ya sea la versión 3 de la Licencia, o (a tu elección) cualquier versión posterior. ¿Desea importar también los ajustes? - Política de Privacidad de NewPipe El proyecto NewPipe toma su privacidad muy en serio. Por lo tanto, la aplicación no recopila ningún dato sin su consentimiento. La política de privacidad de NewPipe explica en detalle qué datos se envían y almacenan cuando envía un informe de fallas. Leer la Política de Privacidad Para cumplir con el Reglamento general europeo de protección de datos (GDPR), podemos llamar su atención sobre la política de privacidad de NewPipe. Por favor léelo cuidadosamente. Debe aceptarlo para enviarnos el informe de error. Aceptar Declinar - Sin límite - Limitar resolución cuando se use Datos Móviles + Limitar la resolución cuando se usen datos móviles Mimimizar al cambiar de aplicación Acción de cambiar a otra aplicación desde el reproductor principal — %s Ninguna Minimizar al reproductor de fondo - Minimizar el reproductor emergente - -Avance rápido durante el silencio + Minimizar al reproductor popup + Avance rápido durante el silencio Paso Reiniciar - Canales Usuarios Listas de reproducción Pistas Finalizadas En cola - pausado en cola post-procesado - Encolar - Acción denegada por el sistema - Archivo borrado - Descarga fallida Descarga finalizada %s descargas finalizadas - Generar nombre único Sobrescribir Ya existe un archivo descargado con este nombre Hay una descarga en curso con este nombre - Mostrar como grilla Mostrar como lista Limpiar descargas finalizadas - Tienes %s descargas pendientes, ve a Descargas para continuarlas + Continúa tus %s transferencias pendientes desde Descargas Detener - Intentos maximos + Intentos máximos Cantidad máxima de intentos antes de cancelar la descarga Pausar al cambiar a datos moviles - No todas las descargas se pueden suspender, en esos casos, se reiniciaran - - + Las descargas que no se pueden pausar serán reiniciadas Mostrar error Codigo @@ -530,12 +436,38 @@ abrir en modo popup No se puede crear el archivo Permiso denegado por el sistema Fallo la conexión segura - No se puede encontrar el servidor + No se pudo encontrar el servidor No se puede conectar con el servidor El servidor no devolvio datos El servidor no acepta descargas multi-hilos, intente de nuevo con @string/msg_threads = 1 - Rango solicitado no satisfactorio + El rango solicitado no se puede satisfacer No encontrado Fallo el post-procesado - - + Desuscribirse + Nueva pestaña + Elige la pestaña + Control de volumen por gestos + Usar gestos para controlar el volumen del reproductor + Control de brillo por gestos + Usar gestos para controlar el brillo del reproductor + Actualizaciones + Eventos + Notificación de actualización de la aplicación + Notificaciones para nueva versión de NewPipe + Almacenamiento externo no disponible + Todavía no es posible descargar a una tarjeta SD externa. ¿Restablecer la ubicación de la carpeta de descarga\? + Usando las pestañas por defecto, error al leer las pestañas guardadas + Restaurar valores por defecto + ¿Quieres restaurar los valores por defecto\? + Número de suscriptores no disponible + Qué pestañas aparecen en la página principal + Selección + Conferencias + Actualizaciones + Mostrar una notificación para solicitar actualizar la aplicación cuando haya una nueva versión disponible + Modo de vista de lista + Automático + Cambiar vista + ¡Actualización de NewPipe disponible! + Pulsa para descargar + \ No newline at end of file From 0e3af45466ae8d5ec9e6bb3c4da6ed2da57bfa1c Mon Sep 17 00:00:00 2001 From: Rex_sa Date: Fri, 1 Mar 2019 14:58:17 +0000 Subject: [PATCH 003/138] Translated using Weblate (Arabic) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-ar/strings.xml | 144 +++++++------------------ 1 file changed, 39 insertions(+), 105 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 882a65af0..f197bf975 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -26,7 +26,7 @@ مضيء صور معاينة الفيديو خطأ في الشبكة - التالى + التالي لا يوجد مشغل فيديو. هل تريد تثبيت VLC ؟ افتح في المتصفح الصوت @@ -38,15 +38,15 @@ الإعدادات المظهر اخرى - الفيديو & الصوتيات + الفيديو و الصوتيات مشاركة مشاركة بواسطة - عرض مقاطع الفيديو \"التالية\" و \"شبيه\" + عرض مقاطع الفيديو \"التالية\" و \"المشابهة\" عرض خيار لتشغيل الفيديو بواسطة مشغل كودي عرض خيار التشغيل بواسطة كودي السمة تم النشر يوم %1$s - رابط غير معتمد URL + رابط URL غير معتمد استخدام مشغل صوت خارجي استخدام مشغل فيديو خارجي (إختبارية) إجراء التنزيلات من خلال استخدام بروكسي Tor لزيادة الخصوصية ( تشغيل الفيديو المباشر غير مدعوم حتى الأن ). @@ -58,19 +58,17 @@ خطأ تعذرت عملية تحليل الموقع تعذر فك تشفير توقيع رابط الفيديو -اضغط بحث للبدء + اضغط بحث للبدء اشتراك مشترك الرئيسية الاشتراكات - ما الجديد - في الخلفية تشغيل تلقائي اسود التاريخ وذاكرة التخزين المؤقت - التاريخ & ذاكرة التخزين المؤقت + التاريخ و ذاكرة التخزين المؤقت المحتوى التنزيلات التنزيلات @@ -83,14 +81,12 @@ التاريخ التاريخ افتح في وضع النافذة المنبثقة - يزيل الصوت في بعض القرارات + يزيل الصوت في بعض الخيارات وضع النوافذ المنبثقة NewPipe تم إلغاء الاشتراك في القناة تعذر تغيير حالة الاشتراك تعذر تحديث الاشتراك - نافذة المنبثقة - تشغيل مقطع الفيديو عند إستدعاء NewPipe من تطبيق آخر الدقة الافتراضية لنوافذ المنبثقة "عرض أعلى جودة " @@ -130,22 +126,18 @@ أفضل دقة تراجع تشغيل الكل - تنبيهات NewPipe تنبيهات مشغل NewPipe للخلفية والنوافذ المنبثقة - [غير معروف] - لا يمكن تحليل الموقع بشكل كلي تعذرت عملية إعداد قائمة التنزيل - لبث المباشر غير مدعوم حتى الآن + البث المباشر غير مدعوم حتى الآن تعذر الحصول على أي بث تعذرت عملية تحميل الصورة تعطل التطبيق / واجهة المستخدم لا يمكن تشغيل هذا البث حدث خطأ للمشغل غير قابل للاسترداد استرداد المشغل من الخطأ - عذرا، لا ينبغي أن يحدث ذلك. الإبلاغ عن خطأ عبر البريد الإلكتروني عذرا، حدثت بعض الأخطاء. @@ -155,31 +147,25 @@ ماذا:\\nطلب:\\nيحتوى اللغة:\\nSالخدمات:\\nتوقيت غرينتش:\\nحزمة:\\nالإصدار:\\nOS إصدار نظام التشغيل: تعليقك (باللغة الإنجليزية): التفاصيل: - - الإبلاغ عن خطأ تقرير المستخدم لم يتم العثور على نتائج لا شيء هنا غير الصراصير - الصوت إعادة المحاولة تم رفض إذن الوصول إلى التخزين - ألف مليون بليون - ليس هناك مشترِكون - %s لا يوجد مشترك - %s مشترك - %s اثنتين مشتركين - %s اشتراكات - %s مشتركين - %s مشتركون - - + %s لا يوجد مشترك + %s مشترك + %s اثنتين مشتركين + %s اشتراكات + %s مشتركين + %s مشتركون + دون مشاهدات لاتوجد فيديوهات ابدأ @@ -187,10 +173,8 @@ شغل حذف التوقيع - مهمة جديدة حسناً - اسم الملف التقسيم الخطأ @@ -204,15 +188,12 @@ الرجاء تحديد مجلد لحفظ التنزيلات هذا الإذن مطلوب \nللفتح في وضع النافذة المنبثقة - اختبار reCAPTCHA السماح بالرموز في أسماء الملفات يتم استبدال الرموز غير المسموح بها بهذه القيمة استبدال الحرف - الحروف والأرقام معظم الأحرف الخاصة - عن تطبيق نيوپايپ الإعدادات تراخيص الجهات الخارجية @@ -231,8 +212,6 @@ قم بزيارة موقع NewPipe لمزيد من المعلومات والمستجدات. تراخيص NewPipe قراءة الرخصة - - التي تم البحث عنها تمت مشاهدتها تم تعطيل السجل @@ -240,7 +219,6 @@ تم مسح التاريخ تم حذف العنصر هل تريد حذف هذا العنصر من سجل البحث؟ - محتوى الشاشة الرئيسية صفحة فارغة صفحة الاشتراك @@ -258,83 +236,68 @@ الإعدادات الصوتية تشغيل هنا تشغيل هنا في وضع النافذة المنبثقة -تحدي الكابتشا + تحدي الكابتشا اضغط للإدراج في قائمة الانتظار - بدون مشاهدات - %s مشاهدة - %s مشاهدتين - %s مشاهدون - %s مشاهدات - %s مشاهدين - - + بدون مشاهدات + %s مشاهدة + %s مشاهدتين + %s مشاهدون + %s مشاهدات + %s مشاهدين + - %s فيديو - %s فيديو - %s فيديوهان - %s فيديو - %s فيديوهات - %s فيديو - - + %s فيديو + %s فيديو + %s فيديوهان + %s فيديو + %s فيديوهات + %s فيديو + طلب اختبار الكابتشا مطلوب - © %1$sبواسطة%2$sتحت%3$s صفحة الكشك حدد كشك - الكشك إدراج في قائمة الانتظار في الخلفية إدراج في قائمة الانتظار على المنبثقة تشغيل في الخلفية -المحتوى الإفتراضي حسب البلد + المحتوى الإفتراضي حسب البلد تغيير الإتجاه الإنتقال إلى التشغيل في الخلفية الإنتقال إلى التشغيل في النافذة المنبثقة الإنتقال إلى الرئيسية - الخدمة فتح الدرج إغلاق الدرج دائماً مرة واحدة فقط - العنوان خاطئ - لم يتم العثور على مشغل تدفق (يمكنك تثبيت VLC وتشغيله). + لم يتم العثور على مشغل الفيديو (يمكنك تثبيت VLC وتشغيله). استيراد قاعدة البيانات تصدير قاعدة البيانات "الكتابة فوق سجل التاريخ والاشتراكات الحالية " تصدير السجل، الإشتراكات وقوائم التشغيل عرض المعلومات - إضافة إلى - تحليل مٌباشِر لم يتم العثور على أي بث مرئي لم يتم العثور على أي بث صوتي - إسحب للقيام بالترتيب - إنشاء إحذف واحدة حذف الكل إلغاء إعادة التسمية - تمت عملية التصدير إكتَملَت عملية الإستيراد تنبيه : تعذرت عملية استيراد كافة الملفات. سوف يظهر شيء هنا قريبا ;D - - مشغل الفيديو السؤال دائماً - الحصول على المعلومات … تحميل المحتوى المطلوب - إنشاء قائمة تشغيل جديدة حذف قائمة التشغيل "إعادة تسمية قائمة التشغيل " @@ -344,68 +307,52 @@ تم إنشاء قائمة التشغيل تمت إضافتها إلى قائمة التشغيل لا يمكن حذف قائمة التشغيل. - ملئ الشاشة تكبير - حجم خط التسمية أصغر خط خط عادي خط ذو حجم كبير - مُزامَنة - تنزيل ملف البث الإشارات المرجعية - استعمال التقديم السريع الغير دقيق "التقديم الغير دقيق يسمح للمشغل بالإطلاع الى الأماكن بشكل اسرع مع دقة اقل " تحميل الصور المصغرة تم إفراغ مساحة ذاكرة التخزين المؤقتة الخاصة بالصور الملف - لا يوجد مثل هذا المجلد لا يمكن أن يكون اسم الملف فارغا طرأ هناك خطأ: %1$s ملف مضغوط ZIP غير صالح إزالة الفواصل المرجعية - تناسب مع الشاشة توليد تلقائي - إستيراد او تصدير إستيراد إستعادة مِن تصدير إلى - عملية الإستعادة جارية … عملية التصدير جارية … - إستيراد ملف "معرفك , soundcloud.com/ الخاص بك " - إفتراضي -عند إيقاف تحميل أي صور مصغرة ، وتوفير البيانات واستخدام الذاكرة. تعمل التغييرات على محو ذاكرة التخزين المؤقت للصورة الموجودة على الذاكرة أو على القرص. + عند إيقاف تحميل أي صور مصغرة ، وتوفير البيانات واستخدام الذاكرة. تعمل التغييرات على محو ذاكرة التخزين المؤقت للصورة الموجودة على الذاكرة أو على القرص. امسح البيانات الوصفية المخزنة مؤقتًا إزالة جميع بيانات صفحات الويب المخزنة مؤقتًا تم محو ذاكرة التخزين المؤقت للبيانات الوصفية وضع البث القادم تلقائيا في قائمة الإنتظار رفض التدفق-التلقائي المشابه في حال كان التدفق السابق يعمل في-حالة عدم التكرار. إضافة صورة مصغرة إلى قائمة التشغيل - تفضيل قائمة التشغيل تم تغيير الصورة المصغرة لقائمة التشغيل. بدون تسميات توضيحية - تسميات توضيحية تعديل مشغل نص التسمية التوضيحية وأنماط الخلفية. يتطلب إعادة تشغيل التطبيق لتصبح التغييرات سارية المفعول. - تمكين LeakCanary قد تتسبب مراقبة تسرب الذاكرة في عدم استجابة التطبيق عند تفريغ السجلات - تقرير الأخطاء خارج دورة الحياة فرض الإبلاغ عن استثناءات Rx غير القابلة للتسليم خارج دورة حياة الجزء أو النشاط بعد التخلص منها - محو سجل المشاهدة احذف سِجل الفيديوهات التي تم تشغيلها حذف سجل المشاهدة بالكامل\? @@ -418,29 +365,21 @@ لا يوجد مثل هذا الملف/مصدر المحتوى الملف غير موجود أو الإذن بالقراءة أو الكتابة إليه غير موجود لا يوجد بث متاح للتنزيل - تم حذف عنصر واحد. - لم يتم تثبيت أي تطبيق لتشغيل هذا الملف - - NewPipe هو برنامج مفتوح المصدر حقوق متروكة: يمكنك استخدام الكود ودراسته وتحسينه كما شئت. و على وجه التحديد يمكنك إعادة توزيعه / أو تعديله تحت شروط رخصة GNU العمومية والتي نشرتها مؤسسة البرمجيات الحرة، سواء الإصدار 3 من الرخصة، أو (باختيارك) أي إصدار جديد. + NewPipe هو برنامج مفتوح المصدر و بحقوق متروكة: يمكنك استخدام الكود ودراسته وتحسينه كما شئت. و على وجه التحديد يمكنك إعادة توزيعه / أو تعديله تحت شروط رخصة GNU العمومية والتي نشرتها مؤسسة البرمجيات الحرة، سواء الإصدار 3 من الرخصة، أو (باختيارك) أي إصدار جديد. هل تريد حذف هذا العنصر من سجل المشاهدة؟ هل أنت متأكد من أنك تريد حذف كل العناصر من السجل؟ آخر ما تم تشغيله الأكثر تشغيلا - هذا سوف يُزيل إعداداتك الحالية. - طريقة \'التشغيل\' المفضلة "الإجراء الافتراضي عند فتح المحتوى — %s" - مشغل الخلفية المشغل المنبثق نسخة احتياطية - تعذر استيراد الاشتراكات لا يمكن تصدير الاشتراكات - استيراد اشتراكات YouTube عن طريق تنزيل ملف التصدير: \n \n1. انتقل إلى عنوان URL هذا: %1$s @@ -455,14 +394,12 @@ ضع في اعتبارك أن هذه العملية يمكن أن تكون مكلفة اذا كنت تستخدم بيانات اشتراك انترنت. \n \nهل تريد الاستمرار؟ - ضوابط سرعة التشغيل سرعة الأداء تردد الصوت نزع الإرتباط (قد يسبب تشويه) تعديل الايقاع Nightcore هل تريد أيضا استيراد الإعدادات؟ - "سياسة الخصوصية في NewPipe " يأخذ مشروع NewPipe خصوصيتك على محمل الجد. لذلك ، لا يجمع التطبيق أي بيانات دون موافقتك. \nتوضح سياسة خصوصية NewPipe بالتفصيل البيانات التي يتم إرسالها وتخزينها عند إرسال تقرير الأعطال. @@ -471,20 +408,17 @@ \nو يجب عليك قبولها لإرسال تقرير الأخطاء إلينا. قبول رفض - لا حدود الحد من جودة الفيديو عند استخدام بيانات الهاتف المحمول تسريع إلى الأمام أثناء الصمت خطوة إعادة تعيين - تصغير عند تبديل التطبيق الإجراء عند التبديل إلى تطبيق آخر من مشغل الفيديو الرئيسي — %s لاشيء تصغير إلى مشغل الخلفية تصغير إلى مشغل منبثق - -القنوات + القنوات قوائم التشغيل المسارات المستخدمين @@ -500,8 +434,8 @@ إيماءة التحكم بالصوت الأحداث التنبيه بإصدارات newpipe الجديدة - ذاكرة التخزين الخارجي غير متوفر - تحميل إلى بطاقة SD الخارجية ليس متاحا حتى الآن. إعادة تعيين موقع مجلد التحميل؟ + ذاكرة التخزين الخارجي غير متوفرة + التحميل إلى بطاقة الذاكرة الخارجية ليس متاحا حتى الآن. إعادة تعيين موقع مجلد التحميل؟ باستخدام علامات الجدولة الافتراضية، حدث خطأ أثناء قراءة علامات التبويب المحفوظة استعادة الافتراضيات هل تريد استعادة الإعدادات الافتراضية؟ @@ -522,7 +456,7 @@ في قائمة الانتظار متوقف في قائمة الانتظار - بعد المعالجة + قيد المعالجة قائمة الانتظار تم رفضها من قبل النظام فشل التنزيل @@ -545,7 +479,7 @@ عدم استيفاء النطاق المطلوب غير موجود فشلت المعالجة الاولية - حذف التنزيل المنتهية + حذف التنزيلات المنتهية "قم بإستكمال %s حيثما يتم التحويل من التنزيلات" توقف أقصى عدد للمحاولات From 891bb7fa40fbd52c63db1677754bb6351947ab94 Mon Sep 17 00:00:00 2001 From: rimasx Date: Sun, 3 Mar 2019 09:33:52 +0000 Subject: [PATCH 004/138] Translated using Weblate (Estonian) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-et/strings.xml | 155 +++++++++++++------------ 1 file changed, 82 insertions(+), 73 deletions(-) diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index bf5cee76b..95065f8e7 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -1,5 +1,6 @@ - -Alustuseks puuduta otsingut + + + Alustuseks puuduta otsingut %1$s vaatamist Avaldatud %1$s Voogesituseks puudub pleier. Kas paigaldada VLC? @@ -27,24 +28,18 @@ Tellimust ei saanud muuta Tellimust ei õnnestunud uuendada Kuva info - Tellimused Esitusloendid järjehoidjates - Mis on uut - Taust Hüpikaken "Lisa " - Video allalaadimise kaust Kaust allalaetud videote hoiustamiseks Sisesta videote allalaadimise rada - Audio allalaadimise kaust Siia salvestatakse alla laaditud audio Sisesta audio allalaadimise rada - Automaatesitus Esita video, kui NewPipe käivitub teise rakenduse kaudu Vaikelahutus @@ -67,15 +62,15 @@ Kasuta ebatäpset kerimist Ebatäpne kerimine lubab pleieril otsida asukohta kiiremini täpsuse arvel Laadi pisipildid - Välujalülitatult ei toimu pisipiltide laadimist, salvestamist ja puhverdamist. Muutmine puhastab vahemälu nii kettal kui ka mälus. + Lülita välja, et keelata pisipiltide laadimist, andmete salvestamist ja mälu kasutust. Muutmine puhastab vahemälu nii kettal kui ka mälus. Pildid kustutati vahemälust Kustuta metaandmed vahemälust Kustuta veebilehtede andmed vahemälust Metaandmed kustutati vahemälust Järgmine voog automaatselt järjekorda Lisa seotud voog automaatselt, kui esitusel on viimane voog mittekorduvast järjekorrast. - Pleieri juhtimise viiped - Luba viiped helitugevuse ja ereduse juhtimiseks + Pleieri juhtimine viibetega + Kasuta helitugevuse ja ereduse reguleerimiseks viipeid Kuva soovitused Kuva otsingu ajal soovitusi Otsinguajalugu @@ -108,7 +103,7 @@ Esita Sisu Vanusepiiranguga sisu - Kuva vanusepiiranguga sisu. Sellise materjali saab lubada \"seadetes\". + Kuva vanusepiiranguga sisu. Sellise materjali saab lubada seadetes. OTSE Allalaadimised Allalaadimised @@ -129,12 +124,9 @@ Alati Üks kord Fail - NewPipe teavitus Teavitused NewPipe tausta- ja hüpikpleierile - [Tundmatu] - Vaheta suunda Lülita taustale Lülita hüpikpleierile @@ -176,7 +168,6 @@ Tühi failinimi pole lubatud Ilmnes viga: %1$s Allalaaditavaid videovooge pole - Vabandust, seda poleks pidanud juhtuma. Teata veast e-posti kaudu Vabandust, ilmnesid mõned vead. @@ -186,8 +177,6 @@ Mis:\\nPäring:\\nSisu Keel:\\nTeenus:\\nGMT aeg:\\nPakett:\\nVersioon:\\nOS versioon: Oma kommentaar (inglise keeles): Üksikasjad: - - Video eelvaate pisipilt Video eelvaate pisipilt Üleslaadiaja avatari pisipilt @@ -200,39 +189,32 @@ Tulemusi pole Siin pole veel midagi Lohista järjestuse muutmiseks - Allalaadimiskataloogi \'%1$s\' loomine nurjus Loodi allalaadimiskataloog \'%1$s\' - Video Audio Proovi uuesti Pääsuõigused salvestile puuduvad Kasuta vana pleierit Vana sisseehitatud mediaframework pleier - K M B - Tellijaid pole - %s tellija - %s tellijat - - + %s tellija + %s tellijat + Pole vaadatud - %s vaatamine - %s vaatamist - - + %s vaatamine + %s vaatamist + Videoid pole - %s video - %s videot - - + %s video + %s videot + "Start " Paus Esita @@ -243,10 +225,8 @@ Kontrollsumma Loobu Nimeta ümber - Uus ülesanne OK - Faili nimi Lõimed Viga @@ -261,18 +241,14 @@ Need õigused on vajalikud \nhüpikakna avamiseks Kustutati 1 element. - "reCAPTCHA " Laadi alla Lubatud tähemärgid failinimedes Vigased tähemärgid asendatakse selle väärtusega Asendustähemärk - Tähed ja numbrid Erimärgid - Selle faili esitamiseks puudub rakendus - NewPipe rakendusest Seaded Programmist @@ -291,8 +267,6 @@ Loe privaatsuspoliitikat NewPipe litsents Loe litsentsi - - Ajalugu Otsitud Vaadatud @@ -306,7 +280,6 @@ Kas kustutada kõik kirjed ajaloost? Viimati esitatud Enim esitatud - Avalehe sisu Tühi leht Kioski leht @@ -322,7 +295,6 @@ Hoiatus: Kõiki faile ei õnnestunud importida. See alistab praeguse seadistuse. Kas importida ka seadistused? - "Kiosk " Trendid "Top 50 " @@ -338,66 +310,48 @@ Alusta taasesitust siit Alusta siit olles taustal Alusta siit uue hüpikaknaga - Ava sahtel Sulge sahtel Siia ilmub varsti midagi ;D - - Lingi avamine Vaikimisi tegevus sisu avamisel — %s - Videopleier Taustapleier Hüpikpleier Küsi alati - Info hankimine… Soovitud sisu laadimine - Uus pleilist Kustuta Nimeta ümber Nimi Lisa pleilisti Määra pleilisti pisipildiks - Lisa pleilist järjehoidjaks Eemalda järjehoidja - Kas kustutada see pleilist? Pleilist loodud Lisati pleilisti Pleilisti pisipilt muudetud. Pleilisti kustutamine nurjus. - Subtiitriteta - Mahuta Täida Suumi - Automaatselt loodud - Subtiitrid Kohanda pleieri subtiitrite teksti suurust ja tausta. Jõustamiseks tuleb rakendus taaskäivitada. - Mälulekke seire võib põhjustada rakenduse hangumise - Import/eksport Import Impordi asukohast Ekspordi asukohta - Import… Eksport… - Impordi fail Eelmine eksport - Tellimuste import nurjus Tellimuste eksport nurjus - Impordi YouTube tellimused eksportfaili abil: \n \n1. Ava URL: %1$s @@ -406,14 +360,11 @@ See toiming võib põhjustada suurt võrguliiklust. \n \nKas jätkata? - Taasesituse kiiruse juhtimine "Tempo " Vaikimisi - Nõustu Keeldu - Piiranguta Piira lahutust mobiilse andmeside kasutamisel Peamenüü @@ -422,10 +373,8 @@ Lood Kasutajad Lülitu peamisele - reCAPTCHA nõue reCAPTCHA nõude taotlus - © %1$s %2$s %3$s alla Vaba kergekaaluline Androidi voogesitus. Kui sul on ideid kujunduse muutmisest, koodi puhastamisest või suurtest koodi muudatustest - abi on alati teretulnud. Mida rohkem tehtud, seda paremaks läheb! @@ -443,13 +392,11 @@ \n3. Logi sisse \n4. Kopeeri suunatud profiili URL. sinu_ID, soundcloud.com/sinu_id - Toon Tühista ühendus (võib põhjustada moonutusi) Keri helitu koht edasi Samm Lähtesta - Selleks, et täita Euroopa Üldist Andmekaitse Määrust (GDPR), juhime tähelepanu NewPipe\'i privaatsuspoliitikale. Palun lugege seda hoolikalt. \nMeile veateate saatmiseks tuleb sellega nõustuda. Minimeeri, kui kasutad teisi rakendusi @@ -457,7 +404,69 @@ Pole Esita taustal Minimeeri hüpikpleierisse - -Jõusta väljaspool fragmenti või elutsüklit olevate kättetoimetamatute Rx erindite raporteerimine nende vabastamise järgselt - - + Jõusta väljaspool fragmenti või elutsüklit olevate kättetoimetamatute Rx erindite raporteerimine nende vabastamise järgselt + Lõpeta tellimine + Uus vahekaart + Vali vahekaart + Helitugevuse juhtimine viibetega + Kasuta helitugevuse reguleerimiseks viipeid + Ereduse reguleerimine viibetega + Kasuta ereduse reguleerimiseks viipeid + Uuendused + Sündmused + Fail kustutati + Rakenduse värskenduse teatis + NewPipe uuest versioonist teavitamine + Väline andmekandja pole saadaval + Allalaadimine välisele SD-kaardile ei ole veel võimalik. Kas lähtestada allalaadimiste kataloogi asukoht\? + Tõrge salvestatud vahekaaride lugemisel. Kasutatakse vaikeväärtusi + Taasta vaikeväärtused + Kas taastada vaikeväärtused\? + Tellijate arv ei ole saadaval + Esilehel kuvatavad vahekaardid + Valik + Konverentsid + Uuendused + Kuva teavitus, kui uus versioon on saadaval + Nimekirjavaate režiim + Nimekiri + Võrgustik + Auto + Vaheta vaadet + NewPipe värskendus on saadaval! + Allalaadimiseks puuduta + Lõpetatud + Järjekorras + peatatud + järjekorras + järeltöötlus + Järjekord + Tegevus keelati süsteemi poolt + Allalaadimine nurjus + Allalaadimine lõpetatud + %s allalaadimist lõppenud + Loo kordumatu nimi + Kirjuta üle + Selle nimega allalaetud fail on juba olemas + Selle nimega allalaadimine on käimas + Näita viga + Kood + Faili ei saa luua + Sihtkausta ei saa luua + Tegevus keelati süsteemi poolt + Turvaline ühendus nurjus + Serverit ei leitud + Serveriga ei saadud ühendust + Server ei saada andmeid + Server ei toeta mitmelõimelisi allalaadimisi. Proovi uuesti kasutades @string/msg_threads = 1 + Mitterahuldav taotletud vahemik + Ei leitud + Järeltöötlemine nurjus + Eemalda lõpetatud allalaadimised + Jätka %s pooleliolevat allalaadimist + Stopp + Uusi katseid kuni + Suurim katsete arv enne allalaadimise tühistamist + Paus üleminekul mobiilsele andmesidele + Allalaadimised, mida ei saa peatada, taaskäivitatakse + \ No newline at end of file From 4d4d776e4d94be7edc3354b92d07f29b0beeee74 Mon Sep 17 00:00:00 2001 From: YONGLE Date: Tue, 5 Mar 2019 03:17:13 +0000 Subject: [PATCH 005/138] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-cmn/strings.xml | 47 +++++++++---------------- 1 file changed, 17 insertions(+), 30 deletions(-) diff --git a/app/src/main/res/values-cmn/strings.xml b/app/src/main/res/values-cmn/strings.xml index 897365ca9..99ac876a9 100644 --- a/app/src/main/res/values-cmn/strings.xml +++ b/app/src/main/res/values-cmn/strings.xml @@ -1,5 +1,6 @@ -点击搜索按钮即可开始使用 + + 点击搜索按钮即可开始使用 %1$s 次观看 发布于 %1$s 找不到媒体播放器。您要安装 VLC 吗? @@ -26,26 +27,20 @@ 退订成功 无法更改订阅 无法更新订阅 - 显示详情 - + 显示信息 主页 订阅 已添加书签到播放列表 - 新功能 - 转到后台 悬浮窗 添加到 - 视频下载路径 储存视频文件的路径 输入视频的下载地址 - 音频下载的路径 下载音频的储存路径 输入音频的下载路径 - 自动播放 NewPipes被其它程序调用时播放视频 默认分辨率 @@ -55,7 +50,7 @@ 用 Kodi 播放 没找到 Kore 应用,需要安装它吗? 显示“用 Kodi 播放”选项 - 显示用 Kodi媒体中心 播放视频的选项 + 显示以 Kodi 媒体中心播放视频的选项 音频 默认音频格式 默认视频格式 @@ -68,10 +63,8 @@ 记住上一次悬浮窗的位置以及大小 已清除图像缓存 最小化悬浮窗播放器 - - -清除观看历史 - 已删除搜索历史 + 清除观看历史 + 搜索记录已删除。 错误 网络错误 举报错误 @@ -85,7 +78,6 @@ 新任务 好 \n - 错误 \n 不支持的服务器 @@ -94,27 +86,22 @@ 请稍等… 字母与数字 最特别的字符 - 这个文件里没有已下载应用程式 - 关于NewPipe 设置 关于 第三方执照 打开网页 删除书签 - 确定要删除该播放列表吗? 已创建播放列表 播放列表 步骤 重置 - 为了遵守欧洲通用数据保护法规(GDPR,我们请你注意NewPipe的隐私政策.请仔细阅读. \n你必须接受它才能将错误报告发送给我们. 接受 拒绝 - 没有限制 使用移动数据时的解析度限制 最小化应用程序切换 @@ -263,19 +250,19 @@ 亿 没有订阅者 - %s 位订阅者 - - + %s 位订阅者 + + 无观看次数 - %s 次观看 - - + %s 次观看 + + 没有视频 - 部视频 - - + 部视频 + + 删除 校验 退出 @@ -289,7 +276,7 @@ 在悬浮窗模式打开 \n需要此权限 已删除一个项目。 - reCAPTCHA + reCAPTCHA 验证 reCAPTCHA 验证 需完成 reCAPTCHA 验证 下载 @@ -412,7 +399,7 @@ \n2.移至该网址:%1$s \n3.当被询问时登入帐号 \n4.复制您重定向的配置文件到网址。 - yourID, soundcloud.com/yourid + 您的 ID,soundcloud.com/yourid 请记住,此操作可能造成昂贵的网络花费。 \n \n您是否要继续? From dd0f3ac6511c6fbd82caf86b9ea9bd154d2cf252 Mon Sep 17 00:00:00 2001 From: Konstantinos Giannopoulos Date: Tue, 5 Mar 2019 17:33:36 +0000 Subject: [PATCH 006/138] Translated using Weblate (Greek) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-el/strings.xml | 154 +++++++++++++------------ 1 file changed, 80 insertions(+), 74 deletions(-) diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 1106baf09..8a2a3403d 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -16,15 +16,12 @@ περιστροφή Χρήση εξωτερικής εφαρμογής αναπαραγωγής βίντεο Χρήση εξωτερικής συσκευής αναπαραγωγής ήχου - Διαδρομή λήψης βίντεο Διαδρομή για αποθήκευση των βίντεο Εισάγετε διαδρομή για λήψη των βίντεο - Διαδρομή λήψης αρχείων ήχου Αυτή είναι η διαδρομή για την αποθήκευση αρχείων ήχου Εισάγετε διαδρομή για λήψη αρχείων ήχου - Προεπιλεγμένη ανάλυση Αναπαραγωγή με το Kodi Η εφαρμογή Kore δεν βρέθηκε. Εγκατάσταση της; @@ -37,7 +34,6 @@ Θέμα Σκοτεινό Φωτεινό - Λήψη Επόμενο Εμφάνιση \"Επόμενου\" και \"Σχετικών\" βίντεο @@ -49,7 +45,6 @@ Αναπαραγωγή στο υπόβαθρο Αναπαραγωγή Σφάλμα δικτύου - Μικρογραφία προεπισκόπισης βίντεο Μικρογραφία προεπισκόπησης βίντεο Μικρογραφία εικόνας προφίλ του χρήστη @@ -57,11 +52,9 @@ Dislike Χρήση του Tor (Πειραματικό) Αναγκάζει την κίνηση λήψης μέσω Tor για αυξημένη προστασία προσωπικών δεδομένων (η αναπαραγωγή δεν υποστηρίζεται ακόμη). - Δεν μπόρεσε να δημιουργηθεί ο φάκελος \'%1$s\' Δημιουργήθηκε ο φάκελος \'%1$s\' -Δ - + Δις Άνοιγμα σε αναδυόμενο παράθυρο Εγγραφή Εγγεγραμμένος @@ -82,8 +75,6 @@ Τι συνέβη: Το σχόλιό σας (στα Αγγλικά): Λεπτομέρειες: - - Αναφορά Σφάλματος Βίντεο Ήχος @@ -100,7 +91,6 @@ Ιστορικό Ιστορικό Εμφάνιση πληροφοριών - Πατήστε στην αναζήτηση για να ξεκινήσετε Δε βρέθηκε πρόγραμμα αναπαραγωγής ροής δεδομένων (μπορείτε να εγκαταστήσετε το VLC για να κάνετε αναπαραγωγή). Κατέβασμα του αρχείου ροής @@ -110,15 +100,12 @@ Αδύνατη η αλλαγή της εγγραφής Αδύνατη η ενημέρωση της εγγραφής Κύριο - Εγγραφές + Συνδρομές Αγαπημένες λίστες αναπαραγωγής - Νέα - Στο παρασκήνιο Αναδυόμενο παράθυρο Προσθήκη σε - Αυτόματη αναπαραγωγή Αναπαραγωγή του βίντεο όταν το NewPipe καλείται από άλλη εφαρμογή Προεπιλεγμένη ανάλυση αναδυόμενου παραθύρου @@ -129,7 +116,7 @@ Χρήση γρήγορης μη-ακριβούς αναζήτησης Η μη-ακριβής αναζήτηση επιτρέπει στην εφαρμογή να αναζητεί θέσεις στο βίντεο γρηγορότερα με μειωμένη ακρίβεια Φόρτωση thumbnails - Αν η επιλογή είναι απενεργοποιημένη δεν φορτώνονται οι μικρογραφίες, χρησιμοποιώντας λιγότερα δεδομένα και μνήμη. Οι αλλαγές σβήνουν την προσωρινή μνήμη των εικόνων. + Με την απενεργοποίηση δεν φορτώνονται οι μικρογραφίες, χρησιμοποιώντας λιγότερα δεδομένα και μνήμη. Οι αλλαγές σβήνουν τις προσωρινά αποθηκευμένες εικόνες στην μνήμη. Εκκαθαρίστηκε η προσωρινή μνήμη εικονών Εκκαθάριση προσωρινά αποθηκευμένων μεταδεδομένων Αφαίρεση όλων των προσωρινά αποθηκευμένων δεδομένων ιστοσελίδων @@ -158,7 +145,7 @@ Προστέθηκε στη λίστα αναπαραγωγής αναδυόμενου παραθύρου Περιεχόμενο Περιεχόμενο περιορισμένης ηλικίας - Βίντεο περιορισμένης πρόσβασης. Για να επιτρέπετε περιερχόμενο τέτοιου είδους, ενεργοποιήστε το στις \"Ρυθμίσεις\". + Βίντεο ηλικιακά περιορισμένης πρόσβασης. Για να επιτρέπετε περιερχόμενο τέτοιου είδους, ενεργοποιήστε το στις \"Ρυθμίσεις\". ΖΩΝΤΑΝΑ Αναφορά σφαλμάτων Κανάλια @@ -176,17 +163,13 @@ Πάντα Μόνο μία φορά Αρχείο - Ειδοποίηση NewPipe Ειδοποιήσεις για την αναπαραγωγή Παρασκηνίου και Αναδυόμενου Παραθύρου - [Άγνωστο] - Αλλαγή προσανατολισμού Αλλαγή σε Παρασκήνιο Αλλαγή σε Αναδυόμενο Παράθυρο Αλλαγή σε Κύριο - Εισαγωγή βάσης δεδομένων Εξαγωγή βάσης δεδομένων Θα παρακάμψει το τρέχον ιστορικό και εγγραφές σας @@ -223,7 +206,6 @@ Το όνομα αρχείου δεν μπορεί να είναι κενό Προέκυψε ένα σφάλμα: %1$s Δεν υπάρχουν διαθέσιμες ροές για λήψη - Λυπούμαστε, αυτό δεν έπρεπε να έχει συμβεί. Αναφορά σφάλματος με ηλεκτρονικό ταχυδρομίο Λυπούμαστε, συνέβησαν κάποια σφάλματα. @@ -232,32 +214,27 @@ Κανένα αποτέλεσμα Δεν υπάρχει τίποτα εδώ Σύρετε για ταξινόμηση - Προσπάθεια εκ νέου Δεν δώθηκε άδεια εγγραφής στην εσωτερική μνήμη Χρήση παλαιάς συσκευής αναπαραγωγής Παλαιά συσκευή αναπαραγωγής Mediaframework - - Κ - Μ + χιλ + Εκ Κανένας εγγεγραμένος χρήστης - %s εγγεγραμένος χρήστης - %s εγγεγραμένοι χρήστες - - + %s εγγεγραμένος χρήστης + %s εγγεγραμένοι χρήστες + Καμία προβολή - %s προβολή - %s προβολές - - + %s προβολή + %s προβολές + Κανένα βίντεο - %s βίντεο - "%s βίντεο " - - + Βίντεο + Βίντεο + Εκκίνηση Αναπαραγωγή Δημιουργία @@ -266,10 +243,8 @@ Άθροισμα ελέγχου Αγνόηση Μετονομασία - Νέα αποστολη ΟΚ - Όνομα αρχείου Νήματα Ο εξυπηρετητής δεν υποστηρίζεται @@ -281,19 +256,14 @@ Αυτή η άδεια είναι απαραίτητη για \nτο άνοιγμα αναδυόμενων παραθύρων 1 αντικείμενο διαγράφηκε. - Αυτόματο τεστ Πρόκληση reCAPTCHA Ζητήθηκε πρόκληση reCAPTCHA - Επιτρεπτοί χαρακτήρες σε ονόματα αρχείων Οι μη έγκυροι χαρακτήρες αντικαθίστανται με αυτήν την τιμή Αντικαταστάτης χαρακτήρας - Οι περισσότεροι ειδικοί χαρακτήρες - Δεν υπάρχει εφαρμογή εγκατεστημένη για την αναπαραγωγή αυτού του αρχείου - Σχετικά με το NewPipe Περί Άδειες Τρίτων @@ -317,8 +287,6 @@ Η άδεια του NewPipe Το NewPipe είναι copylelft ελεύθερο λογισμικό: Μπορείτε να το χρησιμοποιήσετε, να το μελετήσετε, να το μοιραστείτε και να το βελτιώσετε κατά βούληση. Ειδικότερα, μπορείτε να το αναδιανείμετε ή/και να το τροποποιήσετε υπό την άδεια GNU General Public Licence όπως αυτή εκδόθηκε από το Free Software Foundation, είτε υπό την έκδοση 3 της Άδειας είτε (προεραιτικά) υπό οποιαδήποτε μεταγενέστερη άδεια. Διαβάστε την άδεια - - Αναζητημένα Έχει γίνει προβολή Το ιστορικό έχει απενεργοποιηθεί @@ -330,7 +298,6 @@ Είστε σίγουροι ότι θέλετε να σβήσετε όλα τα αντικείμενα από το ιστορικό; Τελευταία αναπαραγωγή Αναπαράχθηκε περισσότερο - Περιεχόμενο της κεντρικής σελίδας Κενή σελίδα Σελίδα περιπτέρου @@ -345,7 +312,6 @@ Προσοχή: Δεν ήταν δυνατή η εισαγωγή όλων των αρχείων. Αυτό θα παρακάμψει τις τρέχουσες ρυθμίσεις σας. Θέλετε επίσης να εισάγετε ρυθμίσεις; - Περίπτερο Συσκευή αναπαραγωγής Παρασκηνίου Συσκευή αναπαραγωγής Αναδυόμενου παραθύρου @@ -358,71 +324,52 @@ Εκκίνηση Αναπαραγωγής εδώ Εκκίνηση εδώ όταν είναι στο Παρασκήνιο Εκκίνηση εδώ όταν είναι στο Αναδυόμενο Παράθυρο - Άνοιγμα Συρταριού Κλείσιμο Συρταριού Κάτι θα παιχτεί εδω σύντομα ;D - - Τοπ 50 Καινούρια & δημοφιλή Προτιμώμενη ενέργεια ανοίγματος Προεπιλεγμένη ενέργεια για το άνοιγμα περιεχομένου — %s - Συσκευή αναπαραγωγής βίντεο Αναπαραγωγή Παρασκηνίου Αναπαραγωγή σε Αναδυόμενο Παράθυρο Πάντα ερώτηση - Γίνεται λήψη πληροφοριών… Γίνεται φόρτωση του ζητούμενου περιεχομένου - Νέα Λίστα Αναπαραγωγής Διαγραφή Μετονομασία Όνομα Προσθήκη στη Λίστα Ορισμός ως μικρογραφία λίστας αναπαραγωγής - Προσθήκη Σελιδοδείκτη στη Λίστα Διαγραφή Σελιδοδείκτη - Διαγραφή αυτής της λίστας αναπαραγωγής; Η λίστα αναπαραγωγής δημιουργήθηκε Προστέθηκε στη λίστα αναπαραγωγής Η μικρογραφία της λίστας αναπαραγωγής άλλαξε. Δεν ήταν δυνατή η διαγραφή της λίστας. - - Δεν υπάρχουν υπότιτλοι - + Χωρίς υπότιτλους Προσαρμογή Γέμισμα Μεγέθυνση - Αυτόματοι - Υπότιτλοι Τροποποίηση του μεγέθους και του φόντου των υπότιτλων. Απαιτεί επανεκκίνηση της εφαρμογής. - Ενεργοποίηση του LeakCanary Η παρακολούθηση των διαρροών μνήμης μπορεί να προκαλέσει την διακοπή της εφαρμογής - Υποχρεωτική αναφορά μη παραδοτέων Rx εξαιρέσεων έξω από το κομμάτι ή τον κύκλο δραστηριότητας μετά από απόρριψη - Εισαγωγή/Εξαγωγή Εισαγωγή Εισαγωγή από Εξαγωγή σε - Γίνεται εισαγωγή… Γίνεται εξαγωγή… - Εισαγωγή αρχείου Προηγούμενη εξαγωγή - Δεν ήταν δυνατή η εισαγωγή των εγγραφών Δεν ήταν δυνατή η εισαγωγή των εγγραφών - Κάντε εισαγωγή των εγγραφών σας στο YouTube κατεβάζοντας το εξής αρχείο: \n \n1. Πλοηγηθήτε στο: %1$s @@ -437,7 +384,6 @@ Αυτή η διαδικασία μπορεί να χρησιμοποιήσει μεγάλο όγκο δεδομένων. \n \nΕπιθυμείτε να συνεχίσετε; - Έλεγχος ταχύτητας αναπαραγωγής Τέμπο Τόνος @@ -446,22 +392,82 @@ Δημοφιλή Αναφορά σφαλμάτων εκτός κύκλου ζωής Το όνομα χρήστη σας, soundcloud.com/όνομαχρήστη - Αποσύνδεση (μπορεί να προκαλέσει παραμόρφωση) Επιτάχυνση αναπαραγωγής κατά τη διάρκεια σιωπής Βήμα Επαναφορά - Προς συμμόρφωση με τον Ευρωπαϊκό Γενικό Κανονισμό για την Προστασία Δεδομένων (GDPR), σας επιστούμε την προσοχή στην πολιτική προστασίας προσωπικών δεδομένων του NewPipe. Παραλούμε, διαβάστε την προσεκτικά. \nΘα πρέπει να την αποδεχτέιτε προκειμένου να μας αποστείλετε την αναφορά σφάλματος. Αποδοχή Απόρριψη - Χωρίς όριο Περιορισμός της ανάλυσης όταν γίνεται χρήση δεδομένων Ελαχιστοποίηση κατά την εναλλαγή εφαρμογών Καμία Ελαχιστοποίηση στο παρασκήνιο Ελαχιστοποίηση σε αναδυόμενο παράθυρο - + Απεγγραφή + Νέα Καρτέλα + Επιλογή Καρτέλας + Ρυθμίσεις χειρονομιών ήχου + Χρησιμοποιήστε χειρονομίες για τον έλεγχο της έντασης του ήχου + Ρυθμίσεις χειρονομιών φωτεινότητας + Χρησιμοποιήστε χειρονομίες για τον έλεγχο της φωτεινότητας + Ενημερώσεις + Συμβάντα + Το αρχείο διαγράφηκε + Ειδοποίηση Ενημέρωσης Εφαρμογής + Ειδοποίηση για νεότερη έκδοση του NewPipe + Εξωτερική μνήμη αποθήκευσης μη διαθέσιμη + Η αποθήκευση στην εξωτερική μνήμη απέτυχε. Επαναφορά στην αρχική τοποθεσία λήψης; + Χρήση προεπιλεγμένων καρτέλων, σφάλμα κατα την ανάγνωση των αποθηκευμένων καρτέλων + Επαναφορά προεπιλεγμένων ρυθμίσεων + Θέλετε να επαναφέρετε τις προεπιλεγμένες ρυθμίσεις; + Το πλήθος των συνδρομητών δεν είναι διαθέσιμο + Ποιές καρτέλες θα εμφανίζονται στην αρχική σελίδα + Επιλογή + Συνέδρια + Ενημερώσεις + Εμφάνιση ειδοποίησης όταν μια υπάρχει μια νεότερη έκδοση + Λειτουργία προβολής ως λίστα + Λίστα + Πλέγμα + Αυτόματα + Αλλαγή τρόπου προβολής + Νεά Έκδοση NewPipe Διαθέσιμη! + Πατήστε για λήψη + Ολοκληρώθηκε + Στην ουρά αναμονής + Παύση + στην ουρά + Μετεπεξεργασία + Ουρά + Η δράση απορρίφθηκε από το σύστημα + Η λήψη απέτυχε + Η λήψη ολοκληρώθηκε + %s λήψεις ολοκρηρώθηκαν + Δημιουργία μοναδικού ονόματος + Αντικατάσταση + Ένα αρχείο με το ίδιο όνομα υπάρχει ήδη + Υπάρχει μια λήψη σε εξέλιξη με αυτό το όνομα + Εμφάνιση σφάλματος + Κωδικός + Το αρχείο δεν μπορεί να δημιουργηθεί + Αδυναμία δημιουργίας φάκελου προορισμού + Η αδειοδότηση απορρίφθηκε απο το σύστημα + Δημιουργία ασφαλής σύνδεσης απέτυχε + Αδυναμία εύρεσης του εξυπηρετητή + Αδυναμία σύνδεσης με τον εξυπηρετητή + Ο εξυπηρετητής δεν μπορεί να στείλει τα δεδομένα + Ο εξυπηρετητής δέν υποστηρίζει πολυνηματικές λήψεις, ξαναπροσπαθήστε με @string/msg_threads = 1 + Το ζητούμενο εύρος δεν μπορεί να εξυπηρετηθεί + Δεν βρέθηκε + Μετεπεξεργασία απέτυχε + Εκκαθάριση ολοκληρωμένων λήψεων + Συνέχιση των %s εκκρεμών σας λήψεων + Διακοπή + Μέγιστες επαναπροσπάθειες + Μέγιστος αριθμός προσπαθειών προτού γίνει ακύρωση της λήψης + Παύση με την εναλλαγή του δικτύου σε δεδομένα + Οι λήψεις που δεν δέχονται παύση θα επανεκκινηθούν \ No newline at end of file From 5412a087fef0ae17a3ff50201f5fb36cd6d34997 Mon Sep 17 00:00:00 2001 From: Michalis Nikolaidis Date: Tue, 5 Mar 2019 18:39:34 +0000 Subject: [PATCH 007/138] Translated using Weblate (Greek) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-el/strings.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 8a2a3403d..2422a493b 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -91,7 +91,7 @@ Ιστορικό Ιστορικό Εμφάνιση πληροφοριών - Πατήστε στην αναζήτηση για να ξεκινήσετε + Πατήστε αναζήτηση για να ξεκινήσετε Δε βρέθηκε πρόγραμμα αναπαραγωγής ροής δεδομένων (μπορείτε να εγκαταστήσετε το VLC για να κάνετε αναπαραγωγή). Κατέβασμα του αρχείου ροής Αφαίρεση του ήχου από κάποιες αναλύσεις @@ -259,7 +259,7 @@ Αυτόματο τεστ Πρόκληση reCAPTCHA Ζητήθηκε πρόκληση reCAPTCHA - Επιτρεπτοί χαρακτήρες σε ονόματα αρχείων + Επιτρεπόμενοι χαρακτήρες σε ονόματα αρχείων Οι μη έγκυροι χαρακτήρες αντικαθίστανται με αυτήν την τιμή Αντικαταστάτης χαρακτήρας Οι περισσότεροι ειδικοί χαρακτήρες @@ -287,8 +287,8 @@ Η άδεια του NewPipe Το NewPipe είναι copylelft ελεύθερο λογισμικό: Μπορείτε να το χρησιμοποιήσετε, να το μελετήσετε, να το μοιραστείτε και να το βελτιώσετε κατά βούληση. Ειδικότερα, μπορείτε να το αναδιανείμετε ή/και να το τροποποιήσετε υπό την άδεια GNU General Public Licence όπως αυτή εκδόθηκε από το Free Software Foundation, είτε υπό την έκδοση 3 της Άδειας είτε (προεραιτικά) υπό οποιαδήποτε μεταγενέστερη άδεια. Διαβάστε την άδεια - Αναζητημένα - Έχει γίνει προβολή + Αναζητήθηκαν + Προβλήθηκαν Το ιστορικό έχει απενεργοποιηθεί Το ιστορικό είναι κενό Το ιστορικό εκκαθαρίστηκε From 389d08c23328db0bdf2603141e7a8d96ed03a0cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Perovi=C4=87?= Date: Mon, 4 Mar 2019 16:15:07 +0000 Subject: [PATCH 008/138] Translated using Weblate (Serbian) Currently translated at 47.4% (210 of 443 strings) --- app/src/main/res/values-sr/strings.xml | 100 +++++-------------------- 1 file changed, 20 insertions(+), 80 deletions(-) diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index fa3011936..5182144e0 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -1,4 +1,4 @@ - + %1$s приказа Објављен %1$s @@ -10,7 +10,7 @@ Преузми Тражи Поставке - Да ли сте мислили: %1$s ? + Да ли сте мислили: %1$s\? Подели помоћу Отвори помоћу ротација @@ -39,21 +39,16 @@ Користи спољашњи видео плејер Користи спољашњи аудио плејер Пуштам у позадини - Пусти - Користи Тор (Експериментално) Принудно преусмерење саобраћаја кроз Тор за доданту приватност (токови још нису подржани). Тема Тамна Светла - Изглед Грешка мреже - Одредиште преузимања за аудио Унесите путању за преузимање аудио фајлова - Путања за упис преузетих аудио фајлова Направљен директоријум за преузимање „%1$s“ Не могу да направим директоријум за преузимање„ %1$s“ @@ -63,15 +58,11 @@ Не могу да рашчланим веб-сајт Садржај није доступан Блокирала ГЕМА - Садржај Прикажи старосно-ограничени садржај Старосно-ограничени видео. Премошћавање је доступно у поставкама. - Не могу да поставим мени преузимања Ово је ТОК УЖИВО, ово још није подржано. - - Не могу да рашчланим веб-сајт у целости Не могу да добавим ниједан ток Нажалост, то не би требало да се деси. @@ -82,17 +73,13 @@ Шта се десило: Ваш коментар (на енглеском): Детаљи: - - Видео Аудио Покушај поново Дозвола за приступ складишту је одбијена Пријавите грешку Извештај корисника - уживо - Тапните на претрагу за почетак Аутопуштање Аутоматско пуштање видеа по позиву друге апликације @@ -101,11 +88,8 @@ Пусти Обриши Хеш - Нова мисија У реду - - Назив фајла Нити Грешка @@ -117,56 +101,39 @@ Сачекајте… Копирано у привремену меморију Одредите доступну фасциклу за преузимање - Преузимања Преузимања Извештај о грешци - Не могох да учитам слику Апликација/УИ је краховала Шта:\\nЗахтев:\\nЈезик садржаја:\\nУслуга:\\nГМТ време:\\nПакет:\\nИздање:\\nИздање ОС-а:\\nГлоб. ИП распон: - Стопка reCAPTCHA стопка - Решите reCAPTCHA стопку - Црна - Сви Канал - - хиљ мил млрд - Да Касније - - Отвори у искачућем режиму Ова дозвола је потребна за отварање у искачућем режиму - Режим прозорчета - Пуштање у режиму прозорчета Искључено - Користи стари плејер Пожељни формат видеа Резолуција искачућег прозора Прикажи више резолуције Само неки уређаји подржавају пуштање 2K/4K видеа - Филтер Освежи Очисти - Позадина Прозорче - Неке резолуције биће БЕЗ звука када је ова опција укључена Упамти величину и позицију искачућег прозора Памти последњу величину и позицију искачућег прозорчета @@ -174,25 +141,20 @@ Користите потезе за управљање осветљајем у јачином звука Предлози у претрази Приказује предлоге током претраге - Искачући прозор Мењање величине - Стара градња Медијафрејмворк плејера -Претплаћен + Претплаћен Претплати Главно Претплате - Шта је ново - Историјат претраге Уписуј појмове претраге локално Историјат Чувај историјат гледања Настави по дохвату фокуса Најбоља резолуција - Преузми О програму Поставке @@ -205,7 +167,6 @@ Погледај на Гитхабу Прочитај лиценцу Допринос - Историјат Тражено Гледано @@ -213,10 +174,7 @@ Историјат Историјат је празан Историјат очишћен - -Настави пуштање након прекида (нпр. позива) - - + Настави пуштање након прекида (нпр. позива) Дозвољени знакови у називима Слова и бројке © %1$s од %2$s под %3$s @@ -224,60 +182,48 @@ Одјављен са канала Не могу да изменим претплату Не могу да ажурирам претплату - Плејер Понашање Историјат Листа пуштања Врати - Обавештење Обавештења за Нупајп у позадини и искачући прозор - Нема резултата Нема ничега овде - Нема претплатника - %s претплатник - %s претплатника - %s претплатника - - + %s претплатник + %s претплатника + %s претплатника + Нема прегледа - %s преглед - %s прегледа - %s прегледа - - + %s преглед + %s прегледа + %s прегледа + Нема видеа - %s видео - %s видеа - %s видеа - - + %s видео + %s видеа + %s видеа + Неисправни знакови ће бити замењени овом вредношћу Знак за замену - Посебни знакови - Лиценце треће стране Лиценца Њупајпа Помоћ је увек добро дошла, било да имате идеју за превод, дизајн, козметичке или озбиљне измене кôда. Што се више уради, боље је! Ставка обрисана -Приказ савета када је позадинско или искачуће дугме притиснуто на страници детаља видеа + Приказ савета када је позадинско или искачуће дугме притиснуто на страници детаља видеа У реду за позадински плејер У реду за искачући плејер Пусти све - Неуспех пуштања овог тока Десила се непоправљива грешка плејера Опорављам се од грешке плејера - Желите ли да обришете ову ставку из историјата претраге? - Садржај на главној страници Празна страница Киоск страница @@ -287,7 +233,6 @@ Изаберите канал Још нема претплата на канал Изаберите киоск - Киоск У тренду Топ 50 @@ -297,22 +242,17 @@ Уклони Детаљи Поставке звука - Нема плејера тока (можете инсталирати ВЛЦ) - Преузимање фајла тока. + Нема плејера тока (можете инсталирати ВЛЦ). + Преузимање фајла тока Прикажи податке - Обележивачи - Додај у - Подразумевана држава за садржај Услуга Исправљање грешака Увек Само једном - [непознато] - Прикажи поруку \"задржи ради стављања у ред\" Донирај ЊуПајп развијају волонтери у своје слободно време како би вама пружили најбоље искуство. Сада је време да им узвратите како би они могли да наставе са побољшавањем ЊуПајпа док уживају пијући кафу! @@ -321,4 +261,4 @@ Да бисте добили више информација и најновије вести о ЊуПајпу посетите наш вебсајт. Задржи за стављање у ред Пусти одавде - + \ No newline at end of file From 27579dff37ecd7d5001582c30b3d395c86fa0da0 Mon Sep 17 00:00:00 2001 From: Marian Hanzel Date: Tue, 5 Mar 2019 20:00:14 +0000 Subject: [PATCH 009/138] Translated using Weblate (Slovak) Currently translated at 88.3% (391 of 443 strings) --- app/src/main/res/values-sk/strings.xml | 198 +++++++++++-------------- 1 file changed, 87 insertions(+), 111 deletions(-) diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index a7359b0d7..c33bfde1b 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -1,4 +1,4 @@ - + %1$s pozretí Zverejnené %1$s @@ -16,14 +16,11 @@ otočenie Použiť externý prehrávač videa Použiť externý prehrávač zvuku - Prevzaté video ukladať do Cesta kam sa budú ukladať prevzaté videá Vložte cestu kam sa budú ukladať videá - Prevzaté audio ukladať do Vložte cestu kam sa budú ukladať zvukové súbory - Cesta kam sa bude ukladať prevzaté audio Štandardné rozlíšenie Prehrať cez Kodi @@ -35,10 +32,9 @@ Téma Tmavá Svetlá - Prevziať - Ďalšie video - Ukázať \'ďalšie\' a \'podobné\' videá + Ďalšie + Ukázať \'Ďalšie\' a \'Podobné\' videá URL nie je podporovaná Preferovaný jazyk obsahu Video & Zvuk @@ -53,7 +49,6 @@ Nemožno analyzovať webovú stránku Obsah nie je dostupný Obsah blokuje GEMA - Náhľad videa Náhľad videa Náhľad avataru uploadera @@ -61,7 +56,6 @@ Dislajky Použiť Tor (Experimentálne) Vyžadovať preberanie cez Tor pre väčšie súkromie (streamovanie videa momentálne nie je podporované). - Nemožno vytvoriť adresár na preberanie \'%1$s\' Adresár na preberanie bol vytvorený \'%1$s\' Začnite vyhľadaním obsahu @@ -71,7 +65,6 @@ Zobraziť vekovo obmedzený obsah Toto video je vekovo obmedzené. Povoľte zobrazenie obsahu v nastavení. naživo - Nemožno kompletne zanalyzovať web Nemožno nastaviť menu preberania Toto je ŽIVÉ VYSIELANIE, ktoré ešte nie je podporované. @@ -84,11 +77,8 @@ Čo sa stalo: Váš komentár (v Angličtine): Detaily: - - Nahlásiť chybu Hlásenie používateľa - Video Zvuk Znova @@ -98,11 +88,8 @@ Prehrať Zmazať Kontrolný súčet - Nová misia OK - - Názov súboru Vlákna Chyba @@ -114,99 +101,72 @@ Čakajte prosím… Skopírované do schránky Vyberte si dostupný adresár na preberanie - Prevzaté Prevzaté Hlásenie o chybe - Nemožno načítať obrázok Aplikácia/UP zlyhalo Čo:\\nPožiadavka:\\nJazyk obsahu:\\nSlužba:\\nČas v GMT:\\nBalík:\\nVerzia:\\nVerzia OS: - reCAPTCHA Výzva reCAPTCHA - Čierna - Všetko Kanál - - K M B - Požiadavka reCAPTCHA - Áno Neskôr - - Spustiť v okne Tieto práva sú potrebné pre \nprehrávanie v mini okne - NewPipe v okne - Preferovaný formát videa Prehrávanie v mini okne Vypnuté - Použiť starší prehrávač Starý zabudovaný prehrávač Mediaframework - Predvolená veľkosť mini okna Zobraziť vyššie rozlíšenie Len niektoré zariadenia podporujú videá 2K/4K Na pozadí V okne - Filter Obnoviť Vyčistiť - -Po zapnutí tejto funkcie niektoré formáty NEBUDÚ obsahovať zvukovú stopu + Odoberie audio pri NIEKTORÝCH rozlíšeniach Zapamätať si veľkosť a pozíciu mini okna Zapamätať si posledné nastavenie veľkosti a pozície mini okna Ovládanie prehrávača gestami Používať gestá pre kontrolu jasu a hlasitosti prehrávača Hľadať návrhy Zobrazovať návrhy pri vyhľadávaní - Mini okno Zmena veľkosti Najlepšie rozlíšenie - Odber Odoberané Odber zrušený Nemožno zmeniť odber Nemožno aktualizovať odber - Hlavné Odbery - Čo je nové - Hľadať v histórií Hľadané výrazy ukladať lokálne História a vyrovnávacia pamäť Ukladať históriu pozretých videí Pokračovať po prepnutí zobrazenia Pokračovať po prerušeniach (napr. hlasový hovor) - - NewPipe notifikácie Notifikácie pre NewPipe na pozadí a v mini okne - Preberanie Povolené znaky v názvoch súborov Neplatné znaky budú nahradené týmito znakmi Náhradný znak - Písmená a číslice Väčšina špeciálnych znakov - O NewPipe Nastavenia O @@ -223,7 +183,6 @@ Či už máte nápady, prekladáte, spravíte zmenu dizajnu, čistenie kódu alebo skutočné ťažké zmeny v kóde, pomoc je vždy vítaná. Čím viac, tým lepšie! Prečítať licencie Zapojiť sa - História Vyhľadávané Pozreté @@ -231,46 +190,37 @@ História História je prázdna História bola vyčistená - -Prehrávač + Prehrávač Správanie História a vyrovnávacia pamäť Zoznam Vrátiť - Žiadne výsledky Zatiaľ je tu ticho ako v hrobe - Žiadni odberatelia - %s odberateľ - %s odberatelia - %s odberateľov - - + %s odberateľ + %s odberatelia + %s odberateľov + Žiadne pozretia - %s pozriete - %s pozretia - %s pozretí - - + %s pozriete + %s pozretia + %s pozretí + Žiadne videá - %s video - %s videá - %s videí - - + %s video + %s videá + %s videí + Položka bola odstránená -Nebol nájdený žiadny prehrávač pre stream (môžete si nainštalovať napr. VLC) - Stiahnuť súbor stream. + Nebol nájdený žiadny prehrávač pre stream (môžete si nainštalovať napr. VLC) + Stiahnuť súbor stream Zobraziť info - - Záložky - + Uložené zoznamy Pridať do - Zobrazovať tip \"Pridať podržaním\" Zobraziť tip pre stlačenie tlačidiel \"Pozadie\" alebo \"V okne\" na stránke videa Predvolený obsah pre krajinu @@ -280,14 +230,11 @@ Prehrať všetko Vždy Iba raz - [Neznámy] - Prehodiť orientáciu Prepnúť na pozadie Prepnúť do mini okna Prepnúť na Video - Importovať databázu Exportovať databázu Prepíše aktuálnu históriu pozretí a odberov @@ -299,15 +246,12 @@ Neplatná URL adresa Neboli nájdené žiadne video streamy Neboli nájdené žiadne audio streamy - Potiahnutím zmeniť poradie - Vytvoriť Odstrániť jeden Odstrániť všetko Zrušiť Premenovať - Prispieť Aplikácia NewPipe je vyvíjaná dobrovoľníkmi vo voľnom čase. Ak sa vám aplikácia páči, odmeňte vývojárov aby mohli NewPipe naďalej vylepšovať. Určite ich poteší napríklad šálka dobrej kávy. Daruj @@ -318,7 +262,6 @@ Ste si istý, že chcete vymazať všetky položky z histórie? Naposledy prehrávané Najprehrávanejšie - Obsah na hlavnej stránke Prázdna strana Kiosk @@ -333,7 +276,6 @@ Neplatný ZIP súbor Upozornenie: Nemožno importovať všetky súbory. Toto prepíše vaše aktuálne nastavenie. - Kiosky Trendy Top 50 @@ -349,45 +291,36 @@ Video odtiaľto Zvuk odtiaľto Mini okno odtiaľto - Otvoriť zásuvku Zavrieť zásuvku - Prehrávač videa Zvuk na pozadí Mini okno Vždy sa opýtať - Získavajú sa informácie… Načítanie požadované obsahu - Vytvoriť nový zoznam skladieb Vymazať zoznam skladieb Premenovať zoznam skladieb Názov Pridať do zoznamu skladieb Nastaviť ako miniatúru zoznamu skladieb - Záložka zoznamu skladieb Odstrániť Záložku - Chcete odstrániť tento zoznam skladieb? Zoznam skladieb vytvorený Pridané do zoznamu skladieb Miniatúra zoznamu skladieb bola zmenená Nemožno odstrániť zoznam skladieb - Bez titulkov - Prispôsobiť Vyplniť Zväčšiť - Veľkosť písma titulkov Menšie Písmo Normálne Písmo Väčšie Písmo -Používať rýchly posun + Používať rýchly posun Rýchly posun umožňuje prejsť na novú pozíciu rýchlejšie, ale s menšou presnosťou Načítanie miniatúr Vypnutím tejto funkcie sa nebudú vytvárať miniatúry a tým sa ušetrí miesto a pamäť. Zmena nastavení spôsobuje vyčistenie vyrovnávacej pamäte @@ -399,33 +332,23 @@ Automaticky zaradí súvisiaci stream, keď prehrávanie začne na poslednom streame v neopakujúcich sa zoznamoch prehrávania Ladenie Súbor - Neplatný adresár Neplatný zdroj súboru/obsahu Súbor buď neexistuje alebo nemáte dostatočné práva na je čítanie či zápis Názov súboru nesme byť prázdny Nastala chyba: %1$s Žiadne streamy nie sú k dispozícii na prevzatie - Prehrávač pre daný typ súboru nebol nájdený - Čoskoro sa tu niečo objaví ;D - - Preferovaná akcia \'otvoriť\' Predvolená akcia pri otváraní obsahu — %s - Automaticky vygenerované - Titulky Upravte mierku textu titulkov a štýly pozadia. Vyžaduje reštart prehrávača - Povoliť službu LeakCanary Monitorovanie pretečenia pamäte môže spôsobiť, že aplikácia nebude reagovať - Nahlásiť mimo-cyklické chyby Vynútiť hlásenie výnimiek nedoručiteľných Rx mimo časového cyklu fragmentov alebo aktivity po zneškodnení - Import/Export \n Import @@ -433,16 +356,12 @@ Importovať z \n Exportovať do - Importovanie… Exportovanie… - Import súboru Predchádzajúci export - Nemožno importovať odbery Nemožno exportovať odbery - Import odberov služby YouTube pomocou exportovaného zoznamu \n \n1. Prejdite na túto adresu URL: %1$s @@ -456,18 +375,16 @@ \n3. Po výzve sa prihláste do svojho účtu \n4. Skopírujte adresu URL, na ktorú ste boli presmerovaní. " vašeID, soundcloud.com/vašeid - Operácia môže byť náročná na počet prenesených dát. \n \nChcete pokračovať? - Ovládanie rýchlosti prehrávania Rýchlosť Výška "Spomalenie (môže spôsobovať skreslenie)" Nightcore režim Predvolené -Vymazať históriu pozretí + Vymazať históriu pozretí Odstráni históriu prehrávaných streamov Vymazať celú históriu pozretí. História pozretí bola vymazaná. @@ -476,18 +393,15 @@ Vymazať celú históriu vyhľadávania. História vyhľadávaní bola vymazaná. 1 položka bola vymazaná. - "NewPipe je slobodný softvér pod licenciou copyleft. Môžete ho používať, študovať a vylepšovať ako len chcete. Konkrétne ho môžete šíriť a/alebo upravovať pod podmienkami Všeobecnej verejnej licencie GNU, ako ju publikuje Free Software Foundation, buď verzia 3 licencie, alebo (podľa vašej voľby) ktorákoľvek neskoršia verzia." Ochrana osobných údajov v NewPipe NewPipe projekt berie vaše súkromie vážne. Preto aplikácia nezhromažďuje žiadne údaje bez vášho súhlasu. \nNewPipe v ochrane súkromia podrobne vysvetľuje, aké údaje budú odoslané a uložené pri hlásení o páde. Prečítajte si pravidlá ochrany osobných údajov Chcete zároveň importovať aj nastavenia? - V súlade s Európskym Všeobecným Nariadením o Ochrane Údajov (GDPR), chceme upriamiť vašu pozornosť na ochranu osobných údajov v NewPipe. Starostlivo si ich prečítajte. Musíte ich prijať pred nahlásením chyby. Prijať Odmietnuť - Bez limitu Limitovať rozlíšenie pri použití mobilných dát Kanály @@ -497,11 +411,73 @@ Pretáčať tiché pasáže Krok Vynulovať - Minimalizovať pri prepnutí aplikácie Akcia pri prepnutí na inú aplikáciu z hlavného prehrávača videa — %s Nič Prehrávať na pozadí Prehrávať v okne - - + Zrušiť odber + Nový panel + Zvoliť panel + Ovládanie hlasitosti gestom + Používať gestá pre ovládanie hlasitosti + Ovládanie jasu gestom + Použivať gestá pre ovládanie jasu + Aktualizácie + Udalosti + Súbor vymazaný + Oznámenie o aktualizácii aplikácie + Upozornenia na novú verziu NewPipe + Externé úložisko je nedostupné + Sťahovanie na externú SD kartu ešte nie je možné. Obnoviť umiestnenie priečinka na prevzatie\? + Chyba pri načítavaní uložených kariet, použijú sa predvolené + Obnoviť predvolené nastavenia + Chcete obnoviť predvolené hodnoty\? + Počet účastníkov nie je k dispozícii + Karty, ktoré sa zobrazujú na hlavnej stránke + Výber + Konferencie + Aktualizácie + Zobrazí sa výzva na aktualizáciu aplikácie, keď je k dispozícii nová verzia + Režim zobrazenia zoznamu + Zoznam + Mriežka + Auto + Prepnúť zobrazenie + Aktualizácia NewPipe je k dispozícii! + Ťuknite a stiahnite + Ukončené + Vo fronte + pozastavené + vo fronte + následné spracovanie + Fronta + Akcia odmietnutá systémom + Sťahovanie zlyhalo + Sťahovanie ukončené + %s sťahovania skončené + Vytvoriť jedinečný názov + Prepísať + Stiahnutý súbor s týmto menom už existuje + Sťahovanie s týmto názvom už prebieha + Zobraziť chybu + Kód + Súbor sa nedá vytvoriť + Cieľový priečinok nie je možné vytvoriť + Povolenie odmietnuté systémom + Bezpečnostné pripojenie zlyhalo + Server sa nepodarilo nájsť + Nepodarilo sa pripojiť k serveru + Server neposiela údaje + Server neakceptuje preberanie viacerých vlákien, zopakujte s @string/msg_threads = 1 + Požadovaný rozsah nie je uspokojivý + Nenájdené + Post-spracovanie zlyhalo + Vyčistiť dokončené sťahovania + Pokračujte v preberaní %s zo súborov na prevzatie + Stop + Maximum opakovaní + Maximálny počet pokusov pred zrušením stiahnutia + Pozastaviť pri prechode na mobilné dáta + Preberania, ktoré nie je možné pozastaviť, budú reštartované + \ No newline at end of file From 06711dc6c38568cc674e1c3dadee90480453fcee Mon Sep 17 00:00:00 2001 From: Marco Szeto Date: Fri, 8 Mar 2019 10:17:44 +0000 Subject: [PATCH 010/138] Translated using Weblate (Chinese (Hong Kong)) Currently translated at 31.8% (141 of 443 strings) --- app/src/main/res/values-zh-rHK/strings.xml | 44 ++++++++-------------- 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 506705536..d1b5fde31 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -1,4 +1,4 @@ - + 安裝 取消 @@ -25,7 +25,6 @@ 主題 黑暗 明亮 - 下載 下一部影片 顯示下一部及相關的影片 @@ -37,7 +36,6 @@ 背景播放 播放 網絡問題 - 影片預覽縮圖 影片預覽縮圖 上載者的個人頭像縮圖 @@ -52,7 +50,6 @@ 聲音下載路徑 已下載的聲音之存放路徑 請輸入聲音檔案的下載路徑 - 未能建立下載路徑「%1$s」 已建立下載路徑「%1$s」 內容被 GEMA 封鎖 @@ -63,7 +60,6 @@ 顯示已設年齡限制的影片 此影片設有年齡限制。若要觀看,請先在設定中解除年齡限制。 直播 - 問題 無法載入全部縮圖 無法為影片地址的簽署解碼。 @@ -80,31 +76,23 @@ 事發經過: 您的意見(請以英文輸入): 詳情: - - 報告問題 用戶報告 - 影片 聲音 重試 下載 下載 問題報告 - 未能提供內容。 存取儲存空間的權限被拒 - 開始 暫停 觀看 刪除 校驗和 - 新任務 - - 檔案名稱 線程 錯誤 @@ -116,16 +104,13 @@ 請稍候… 已複製至剪貼板 請選擇下載資料夾。 - 在畫中畫模式開啟 NewPipe 畫中畫模式 - 預設畫中畫解像度 顯示更高解像度 只有某些裝置能夠播放 2K 或 4K 影片 預設影片檔案格式 純黑 - 以畫中畫模式播放 所有 頻道 @@ -135,39 +120,30 @@ 應用程式或介面出現問題 事件:\\n請求:\\n內容語言:\\n服務:\\nGMT 時間:\\nPackage:\\n版本:\\n作業系統版本: 使用舊播放器 - - K M - reCAPTCHA reCAPTCHA 挑戰 畫中畫模式需要此權限 - 需完成 reCAPTCHA 挑戰 - -啟用此選項將導致某些解像度的影片失去聲音 + 啟用此選項將導致某些解像度的影片失去聲音 背景播放 畫中畫播放 - 記住畫中畫大小及位置 記住最近設定的畫中畫大小及位置 以動作控制播放器 使用動作以控制播放器的亮度及音量 搜尋建議 進行搜尋時顯示建議 - 畫中畫 過濾 不適用 重新整理 清除 最佳解像度 - 調整大小 使用舊的內置 Mediaframework 播放器 B - 關於 NewPipe 設定 關於 @@ -184,5 +160,17 @@ 無論您僅想分享您對 NewPipe 的一些構思,還是願意設計和翻譯程式介面,甚至想幫我們整理或重新編寫原始碼,我們都無任歡迎。貢獻更多,應用程式便會變得更好! 檢閱特許 貢獻 -清除觀看歷史 - + 清除觀看歷史 + 找不到串流播放器 (您可以安裝並使用VLC播放)。 + 下載串流檔案 + 訂閱 + 已訂閱 + 取消訂閱 + 已成功取消訂閱頻道 + 無法變更訂閱 + 無法更新訂閱 + 顯示資訊 + 首頁 + 訂閱項目 + 已收藏播放清單 + \ No newline at end of file From 2f38943488c8026fad5a4762a2381969587cf62d Mon Sep 17 00:00:00 2001 From: Terry Louwers Date: Fri, 8 Mar 2019 15:52:15 +0000 Subject: [PATCH 011/138] Translated using Weblate (Dutch) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-nl/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index db3eb00df..3bf219912 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -155,7 +155,7 @@ Over Bijdragers Licenties - Vrij en lichtgewicht streamen op Android. + Vrij en licht streamen voor Android. Bekijken op GitHub Licentie van NewPipe Hulp is altijd welkom. Of je nu nieuwe ideeën hebt, vertalingen kan aanleveren, wijzigingen in het ontwerp kan verrichten, code kan opschonen of van grote wijzigingen voorzien. Hoe meer hulp, hoe beter het wordt! From 1a6b915112d312932eb2d4bf7aa51bd094f9bcdd Mon Sep 17 00:00:00 2001 From: WaldiS Date: Fri, 8 Mar 2019 08:39:14 +0000 Subject: [PATCH 012/138] Translated using Weblate (Polish) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-pl/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index e1c03d75b..27a398e60 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -464,10 +464,10 @@ Wyczyść ukończone pobieranie Kontynuuj %s oczekujące transfery z plików do pobrania Zatrzymaj - Maksymalna liczba prób + Maksymalna liczba powtórzeń Maksymalna liczba prób przed anulowaniem pobierania Przerwij przełączanie na dane mobilne - Pobierane pliki, których nie można wstrzymać, zostaną ponownie uruchomione + Pobierane pliki, których nie można wstrzymać, zostaną zrestartowane Zdarzenia Konferencje \ No newline at end of file From c46e3cf5cb26616f9eb988dac5ee6c0c2af4cf75 Mon Sep 17 00:00:00 2001 From: marciozomb13 Date: Thu, 7 Mar 2019 18:36:12 +0000 Subject: [PATCH 013/138] Translated using Weblate (Portuguese (Brazil)) Currently translated at 99.5% (441 of 443 strings) --- app/src/main/res/values-pt-rBR/strings.xml | 111 +++++---------------- 1 file changed, 23 insertions(+), 88 deletions(-) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 2c73758b1..6ec9ced28 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -112,7 +112,6 @@ Relatório do usuário Exibir opção \"Reproduzir com Kodi\" O que:\\nRequisição:\\nIdioma do conteúdo:\\nServiço:\\nHora GMT:\\nPacote:\\nVersão:\\nVersão SO: - Abrir em modo popup Resolução padrão de popup Mostrar resoluções maiores @@ -124,37 +123,27 @@ Sim Depois Desabilitado - K M B - Essa permissão é necessária para abrir em modo popup - Modo de popup NewPipe - Filtro Atualizar Limpar - Popup - Plano de fundo Lembrar tamanho e posição do popup Lembrar do último tamanho e posição definido para o popup - Popup Redimensionamento - Remove o áudio em ALGUMAS resoluções Controle do player por gestos Usar gestos para controlar o brilho da tela e volume do player Sugestões de busca Mostrar sugestões quando pesquisar - -Melhor resolução - + Melhor resolução Configurações Sobre Licenças de Terceiros @@ -169,30 +158,24 @@ abrir em modo popup Sempre que tiver ideias, traduções, mudanças do design, limpeza de código ou grandes alterações de código, a ajuda é bem vinda. Quanto mais for feito, melhor o aplicativo fica! Ler licença Contribuir -© %1$s %2$s protegido pela licença %3$s + © %1$s %2$s protegido pela licença %3$s Sobre NewPipe Download Caracteres permitidos em nome de arquivos Caracteres inválidos são substituídos por este valor Caractere de substituição - Letras e dígitos Caracteres especiais - Inscrever-se Inscrito Inscrição ao canal cancelada Não foi possível modificar inscrição Não foi possível atualizar inscrição - Principal Inscrições - Novidades - Retomar reprodução ao ganhar foco Continuar reproduzindo depois de interrupções (exemplo: ligações) - Histórico de pesquisas Armazenar histórico de pesquisa localmente Histórico & Cache @@ -204,40 +187,33 @@ abrir em modo popup Histórico O histórico está vazio Histórico limpo - -Notificações do NewPipe + Notificações do NewPipe Notificações em plano de fundo para o NewPipe e players em popup - Comportamento Histórico e Cache Playlist Desfazer - Nenhum resultado Nenhum inscrito - %s inscrito - %s inscritos - - + %s inscrito + %s inscritos + Nenhuma visualização - %s visualização - %s visualizações - - + %s visualização + %s visualizações + Nenhum video - %s Vídeo - %s Vídeos - - + Vídeo + Vídeos + Item excluído -Reprodutor + Reprodutor Não há nada aqui - Deseja apagar este item do seu histórico de busca? -Conteúdo + Conteúdo Página em Branco Página de Quiosque Página de Inscrição @@ -246,35 +222,31 @@ abrir em modo popup Selecione um canal Nenhuma inscrição ainda Selecione um quiosque - Quiosque Em Alta Top 50 Novos e tendências -Mostrar dica \"Mantenha pressionado para enfileirar\" + Mostrar dica \"Mantenha pressionado para enfileirar\" Mostrar dica quando o botão de plano de fundo ou de popup for pressionado na página de detalhes do vídeo Adicionado a fila do reprodutor em plano de fundo Adicionado a fila no reprodutor popup Reproduzir tudo - Não foi possível reproduzir esta stream Ocorreu um erro no reprodutor Recuperando erro do reprodutor - Reprodutor de Plano de Fundo Reprodutor Popup Remover Detalhes Configurações de Áudio Mantenha pressionado para colocar na fila -[Desconhecido] - + [Desconhecido] Adicionar a fila em Plano de Fundo Adicionar a fila em Popup Iniciar a reproduzir à partir daqui Iniciar aqui quando estiver em Plano de Fundo Iniciar aqui em novo popup -Doar + Doar NewPipe é desenvolvido por voluntários que usam seu tempo para trazer a melhor experiência para você. Retribua para ajudar os desenvolvedores a tornarem o NewPipe ainda melhor enquanto desfrutam uma xícara de café. Retribuir Site oficial @@ -284,24 +256,20 @@ abrir em modo popup Serviço Sempre Apenas esta vez - Alterar a orientação Alterar para Plano de Fundo Alterar para Popup Alterar para o Principal - Reprodutores externos não suportam estes tipos de links URL inválida Nenhum stream de vídeo encontrado Nenhum stream de áudio encontrado - Reprodutor de vídeo Reprodutor em plano de fundo Reprodutor popup - Obtendo informações… Carregando o conteúdo requisitado -Importar base de dados + Importar base de dados Exportar base de dados Sobrescreve seu histórico e inscrições Exportar histórico, inscrições e listas de reprodução @@ -310,88 +278,64 @@ abrir em modo popup Não há nenhum arquivo ZIP válido Aviso: Não foi possível importar todos arquivos. Isso irá sobrescrever suas configurações atuais. - Baixar arquivo de stream Mostrar informações - Playlists favoritas - Adicionar a - Arraste para ordenar - Criar Deletar um Deletar todos Dispensar Renomear - Deseja apagar este item do seu histórico de visualizações? Tem certeza que deseja apagar todos itens do histórico? Reproduzido anteriormente Mais vezes reproduzido - Sempre perguntar - Nova Lista de Reprodução Apagar Renomear Nome Adicionar a Lista de Reprodução Definir como Miniatura da Lista de Reprodução - Favoritar Lista de Reprodução Remover Favorito - Deletar esta lista de reprodução\? Lista de Reprodução criada Adicionado a lista de reprodução Miniatura da lista de reprodução alterada. Falha ao apagar lista de reprodução. - Sem Legendas - Ajustar Preencher Zoom Algo irá aparecer aqui em breve ;D - - Gerado automaticamente Habilitar LeakCanary "Monitoramento de vazamento de memória pode fazer com que o app fique sem responder quando estiver despejando a pilha " - Reportar Erros de Fora do Ciclo de Vida Forçar o report de exceções Rx não entregáveis ocorrendo fora do fragmento ou ciclo de vida da atividade após o dispose - -Usar índice de indexação rápido porém não preciso + Usar índice de indexação rápido porém não preciso Usar índice de indexação inexato Adicionar a próxima stream à fila automaticamente Auto anexar uma stream relacionada quando a reprodução iniciar na última stream em uma fila não repetitiva. - - Arquivo - Pasta não encontrada Origem do arquivo/conteúdo não econtrada Arquivo não existe ou não há permissão para ler ou escrever nele Nome do arquivo não pode ser vazio Um erro ocorreu: %1$s - Importar/Exportar Importar Importar de Exportar para - Importando… Exportando… - Importar arquivo Exportação anteriore - Importação de inscrições falhou Exportação de inscrições falhou - "Importe as inscrições da sua conta no YouTube através do arquivo exportado por ela em: \n \n1. Vá para este link: %1$s @@ -404,11 +348,10 @@ abrir em modo popup \n3. Faça login quando solicitado \n4. Copie o link no qual você foi redirecionado (este é o link do seu perfil). seuID, soundcloud.com/seuid - Tenha em mente que esta operação poderá usar bastante a conexão com a internet. \n \nVocê deseja continuar? -Carregar miniaturas + Carregar miniaturas Cache de imagem foi limpo Limpar o cache de metadados Remover todos os dados de páginas em cache @@ -418,18 +361,14 @@ abrir em modo popup Quando desligado miniaturas não carregarão, economizando uso de dados e memória. Alterar esta configuração irá limpar o cache de imagens na memória e em disco. Passo Desvincular (pode causar distorção) -Ação de \'abrir\' preferida + Ação de \'abrir\' preferida Ação padrão quando abrir conteúdo — %s - Sem fontes disponíveis para download - Abrir gaveta Fechar gaveta Legendas Modifique o tamanho da legenda e o estilo da tela de fundo. Necessário reiniciar o aplicativo para ter efeito. - Nenhum player instalado para reproduzir este arquivo - "Limpar histórico de já assistidos " Deleta o histórico de videos já reproduzidos Deletar todo o histórico de já reproduzidos\? @@ -439,10 +378,8 @@ abrir em modo popup Deletar todo o histórico de pesquisa\? Histórico de pesquisa deletado. 1 item deletado. - NewPipe é software livre copyleft: Você pode usar, estudar, compartilhar e melhorar ele a vontade. Mais especificamente você pode redistribuir e/ou modificar ele sob os termos da GNU General Public License como publicada pela Free Software Foundation, tanto a versão 3 dessa Licença, ou (a sua escolha) qualquer outra versão posterior. Você também quer importar as configurações? - Política de privacidade do NewPipe O projeto NewPipe leva a sua privacidade muito a sério. Sendo assim, o aplicativo não coleta nenhum dado sem seu consentimento. \nA polícia de privacidade do NewPipe explica em detalhes qual dado é enviado e salvo quando você envia um relatório de erros. @@ -451,19 +388,17 @@ abrir em modo popup \nVocê tem que aceitá-la para nos enviar relatório de erros. Aceitar Recusar -Ilimitado + Ilimitado Limitar resolução quando dados móveis estiverem em uso Minimizar ao trocar de aplicativo Ação ao trocar de aplicativo quando estiver no reprodutor de vídeo principal — %s "Nenhuma " Minimizar para reprodutor em plano de fundo Minimizar para reprodutor popup - -Avançar rapidamente durante silêncio + Avançar rapidamente durante silêncio Parar \n Reiniciar - Canais Listas de reprodução Faixas From b01fc1be624c0f0ecca9681637d1649e86064fc5 Mon Sep 17 00:00:00 2001 From: Pj Pj Date: Thu, 7 Mar 2019 09:44:34 +0000 Subject: [PATCH 014/138] Translated using Weblate (Telugu) Currently translated at 28.7% (127 of 443 strings) --- app/src/main/res/values-te/strings.xml | 56 +++++++++----------------- 1 file changed, 20 insertions(+), 36 deletions(-) diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml index b566977d5..5915aee7a 100644 --- a/app/src/main/res/values-te/strings.xml +++ b/app/src/main/res/values-te/strings.xml @@ -1,6 +1,6 @@ - + -ప్రారంభించడానికి శోధనను క్లిక్ + ప్రారంభించడానికి శోధనను క్లిక్ %1$s వీక్షణలు %1$s ప్రచురించబడింది ఇన్స్టాల్ @@ -10,35 +10,29 @@ డౌన్లోడ్ శోధిచు సెట్టింగ్‌లు - %1$s ? అంటే నువ్వు అనేది + అంటే నువ్వు అనేది తో పంచు బ్రౌజర్ను ఎంచుకోండి - భ్రమణ + రొటేషన్ బాహ్యట ఆడియో ప్లేయర్ని ఉపయోగించండి న్యూపైప్ పాపప్ మోడ్ \n - సభ్యత్వ + "సభ్యత్వం " సబ్ స్క్రైబ్ - అన్ సబ్స్క్రైబ్ చేయబడింది + అన్ సబ్స్క్రైబ్ చెసిరు సబ్ స్క్రైబ్ సాధ్యం కాలేదు సబ్ స్క్రైబ్ నవీకరించలేరు - ప్రధానంగా సభ్యత్వం - కొత్తది ఏమిటి - వెనకవైపు పాపప్ - వీడియో డౌన్లోడ్ మార్గం డౌన్లోడ్ చేసిన వీడియోలను నిల్వ చేయడానికి మార్గం వీడియోల కోసం డౌన్లోడ్ మార్గాన్ని నమోదు చేయండి - ఆడియో డౌన్లోడ్ మార్గం ఆడియో కోసం డౌన్లోడ్ మార్గాన్ని నమోదు చేయండి ఆడియో ఫైల్లకు డౌన్లోడ్ మార్గాన్ని ఇవ్వండి - దానంతట అదే ఆడుతుంది కోడితో ప్లే చేయండి కోరే అనువర్తనం కనుగొనబడలేదు. దీన్ని ఇన్స్టాల్ చేయండి @@ -76,7 +70,6 @@ తర్వాత రిఫ్రెష్ అన్నింటినీ ప్లే చేయండి - న్యూప్యాప్ నోటిఫికేషన్ లోపం నెట్వర్క్ లోపం @@ -89,7 +82,6 @@ చిత్రాన్ని లోడ్ చేయడం సాధ్యపడలేదు ఈ స్ట్రీమ్ని ప్లే చేయడం విఫలమైంది ఆటగాడు లోపం నుండి పునరుద్ధరించడం - క్షమించాలి, అది జరగకూడదు ఇ-మెయిల్ ద్వారా నివేదన లోపం క్షమించండి, కొన్ని లోపాలు సంభవించాయి @@ -98,8 +90,6 @@ ఏం జరిగింది మీ వ్యాఖ్య(ఆంగ్లం లో) వివరాలు - - వీడియో ప్రివ్యూ సూక్ష్మచిత్రం Video preview thumbnail ఇష్టాలు @@ -116,32 +106,27 @@ కె ఎం బి - చందాదారులు లేరు - %s సబ్స్క్రయిబ్ - %s సబ్స్క్రయిబలు - - + %s సబ్స్క్రయిబ్ + %s సబ్స్క్రయిబలు + వీక్షణలు లేవు - %s వీక్షణ - %s వీక్షణలు - - + %s వీక్షణ + %s వీక్షణలు + వీడియోలు లేవు - %s వీడియో - %s వీడియోలు - - + %s వీడియో + %s వీడియోలు + ప్రారంభం ఆపు ప్లే తొలగించు కొత్త మిషన్ అలాగే - ఫైలుపేరు థ్రెడ్లు లోపం @@ -156,7 +141,6 @@ ఫైల్ పేర్లలో అనుమతించిన అక్షరాలు చెల్లని అక్షరాలు ఈ విలువతో భర్తీ చేయబడతాయి ప్రత్యామ్నాయం పాత్ర - లెటర్స్ మరియు అంకెలు న్యూపిప్ గురించి సెట్టింగులు @@ -172,7 +156,6 @@ మీరు ఆలోచనలు ఉన్నాయా లేదో; అనువాదం, డిజైన్ మార్పులు, కోడ్ క్లీనింగ్ లేదా రియల్ భారీ కోడ్ మార్పులు-సహాయం ఎల్లప్పుడూ స్వాగతం. మరింత అది గెట్స్ మంచి జరుగుతుంది లైసెన్స్ చదువు కాంట్రిబ్యూషన్ - చరిత్ర శోధించింది వీక్షించారు @@ -182,7 +165,6 @@ చరిత్ర క్లియర్ చేయబడింది అంశం తొలగించబడింది మీరు ఈ అంశాన్ని శోధన చరిత్ర నుండి తొలగించాలనుకుంటున్నారా? - ప్రధాన పేజీ యొక్క కంటెంట్ ఖాళీ పేజీ చందా పేజీ @@ -197,8 +179,10 @@ వివరాలు ఆడియో సెట్టింగ్లు ఎన్క్యూలో పట్టుకోండి -"మీదగార వీడియో కి కావాల్సిన ప్లేయర్ లేదు. VLC ప్లేయర్ ఇన్స్టాల్ చేసుకుంటారా?" - "మీదగార వీడియో కి కావాల్సిన ప్లేయర్ లేదు (మీరు VLC ఇసన్తాల్ చేసుకోవచ్చు)" + "మీదగార వీడియో కి కావాల్సిన ప్లేయర్ లేదు. VLC ప్లేయర్ ఇన్స్టాల్ చేసుకుంటారా?" + "మీదగార వీడియో కి కావాల్సిన ప్లేయర్ లేదు (మీరు VLC ఇసన్తాల్ చేసుకోండి )" పాపప్ మోడ్ తెరవండి డిఫాల్ట్ పాపప్ స్పష్టత - + ప్రసార ఫైల్ను డౌన్లోడ్ చేయండి + బాహ్య వీడియో ప్లేయర్ని ఉపయోగించండి + \ No newline at end of file From c4d8eae547ef3afc87221ff3a0c92a80b1aae2be Mon Sep 17 00:00:00 2001 From: 84436 Date: Fri, 8 Mar 2019 06:56:39 +0000 Subject: [PATCH 015/138] Translated using Weblate (Vietnamese) Currently translated at 99.3% (440 of 443 strings) --- app/src/main/res/values-vi/strings.xml | 36 ++++++++++++++------------ 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index df246a5df..f200bdd72 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -16,11 +16,11 @@ Chia sẻ với Chọn trình duyệt Sử dụng trình phát video bên ngoài - Loại bỏ âm thanh ở một vài độ phân giải video + Âm thanh có thể không có ở *một vài* độ phân giải video Sử dụng trình phát audio bên ngoài Chế độ popup của NewPipe - Popup - Đường dẫn tải về video + + Thư mục tải về video Đường dẫn để lưu video đã tải về Nhập đường dẫn tải về cho video Thư mục tải về audio @@ -31,14 +31,14 @@ Độ phân giải mặc định Độ phân giải popup mặc định Hiển thị độ phân giải cao hơn - Chỉ một số thiết bị hỗ trợ chơi các video 2K / 4K + Chỉ một số thiết bị hỗ trợ phát video 2K/4K Phát với Kodi Không tìm thấy ứng dụng Kore. Bạn có muốn cài đặt nó\? Hiển thị tùy chọn \"Phát với Kodi\" Hiển thị tùy chọn để phát video qua trung tâm media Kodi Âm thanh - Định dạng âm thanh mặc định - Định dạng video ưa thích + Định dạng audio mặc định + Định dạng video mặc định Nền Chủ đề Sáng @@ -52,7 +52,7 @@ Hiển thị các đề xuất khi tìm kiếm Tải về Tiếp - Hiển thị video \'Tiếp\' và \'Tương tự\' + Hiển thị video \"Tiếp theo\" và \"Tương tự\" URL không được hỗ trợ Hiển thị Khác @@ -60,7 +60,7 @@ Phát ở chế độ popup Phát Nội dung - Nội dung có giới hạn độ tuổi + Cho phép nội dung có giới hạn độ tuổi Hiện video có giới hạn độ tuổi. Có thể bật cài đặt này trong phần Cài đặt. TRỰC TIẾP Tải xuống @@ -160,7 +160,7 @@ xoay màn hình Ngôn ngữ nội dung ưu tiên Video & âm thanh - Popup + Lịch sử & bộ nhớ cache Lịch sử & bộ nhớ cache Playlist @@ -178,8 +178,8 @@ Playlist đã đánh dấu Có gì mới Thêm vào - Sử dụng tìm kiếm không chính xác nhanh - Tìm kiếm không chính xác cho phép người chơi tìm kiếm vị trí nhanh hơn với độ chính xác giảm + Sử dụng tìm kiếm nhanh không chính xác + Tìm kiếm không chính xác cho phép trình phát tua đến vị trí nhanh hơn với độ chính xác bị hạn chế Tải hình thu nhỏ Tắt để không tải về các hình thu nhỏ, tiết kiệm lưu lượng mạng và bộ nhớ. Thay đổi điều này sẽ xóa bộ nhớ đệm hình ảnh cả trong RAM và trong bộ nhớ. Đã xóa bộ nhớ cache hình ảnh @@ -191,7 +191,7 @@ Lịch sử tìm kiếm Lưu trữ truy vấn tìm kiếm cục bộ Theo dõi các video đã xem - Tiếp tục lấy tiêu điểm + Tiếp tục phát sau khi bị gián đoạn Tiếp tục phát sau khi bị gián đoạn (ví dụ: cuộc gọi điện thoại) Hiển thị mẹo \"Giữ để nối thêm\" Hiển thị mẹo khi nhấn nút phát trong nền hoặc phát trên popup trong trang chi tiết video @@ -267,7 +267,7 @@ Các ký tự được cho phép trong tên tệp Ký tự không hợp lệ được thay thế bằng giá trị này Ký tự thay thế - Chữ cái và chữ số + Chỉ chữ cái và chữ số Hầu hết các ký tự đặc biệt Không có ứng dụng nào được cài đặt để phát tệp này Đóng góp @@ -396,16 +396,16 @@ Giới hạn độ phân giải khi sử dụng dữ liệu di động Thu nhỏ khi chuyển qua ứng dụng khác Hành động khi chuyển sang ứng dụng khác từ trình phát chính — %s - không ai + Không Thu nhỏ xuống trình phát nền Thu nhỏ vào trình phát popup Hủy đăng ký Tab mới Chọn tab Điều khiển âm lượng bằng cử chỉ - Sử dụng cử chỉ để điều khiển âm lượng của trình nghe nhạc + Sử dụng cử chỉ để điều khiển âm lượng của trình phát Điều khiển độ sáng màn hình bằng cử chỉ - Sử dụng cử chỉ để điều khiển độ sáng màn hình của trình phát nhạc + Sử dụng cử chỉ để điều khiển độ sáng màn hình của trình phát Cập nhật Sự kiện Đã xóa tập tin @@ -419,7 +419,7 @@ Số người đăng ký không khả dụng Chọn các tab để hiện trên trang chủ Lựa chọn - + Nhập ID SoundCloud hoặc link soundcloud.com/<ID của bạn> Cập nhật Hiện thông báo khi có bản cập nhật ứng dụng @@ -463,4 +463,6 @@ Số lượt thử lại trước khi hủy tải về Tạm dừng tải khi chuyển qua dữ liệu di động Các tải về không thể tạm dừng được sẽ bắt đầu lại từ đầu + Hội thảo + \ No newline at end of file From 1315da0da7d720b95b87216ab8b7200c2d2f69cc Mon Sep 17 00:00:00 2001 From: ssantos Date: Sat, 9 Mar 2019 19:51:30 +0000 Subject: [PATCH 016/138] Translated using Weblate (German) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-de/strings.xml | 127 ++++--------------------- 1 file changed, 21 insertions(+), 106 deletions(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index e233e017b..319e5e8e6 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -39,19 +39,15 @@ Externen Audio-Player verwenden Spiele im Hintergrund ab Abspielen - Benutze Tor (Experimentell) Erzwinge das Herunterladen über Tor für verbesserte Privatsphäre (Videostream werden noch nicht unterstützt). Netzwerkfehler - Downloadverzeichnis für Audiodateien Heruntergeladene Audiodateien werden hier gespeichert Downloadverzeichnis für Audiodateien angeben - Design Dunkel Hell - Aussehen Andere Kann Downloadverzeichnis \'%1$s\' nicht anlegen @@ -62,15 +58,11 @@ Konnte Webseite nicht analysieren Inhalt nicht verfügbar Durch die GEMA gesperrt - Inhalt Altersbeschränkte Inhalte Altersbeschränktes Video anzeigen. Das Zulassen dieses Materials ist von den Einstellungen aus möglich. - Konnte Download-Menü nicht einrichten Live-Streams werden noch nicht unterstützt - - Konnte Webseite nicht vollständig analysieren Fehler via E-Mail melden MELDEN @@ -78,7 +70,6 @@ Dies ist passiert: Was:\\nAnfrage:\\nSprache des Inhalts:\\nDienst:\\nZeit (GMT):\\nPaket:\\nVersion:\\nOS-Version: Details: - Video Audio Wiederholen @@ -91,30 +82,22 @@ Spiele ein Video ab, wenn NewPipe von einer anderen App aufgerufen wurde Einen Fehler melden Anwenderbericht - LIVE - - „Suchen“ antippen, um zu beginnen + Suchen antippen, um zu beginnen Downloads Downloads Fehlerbericht - Löschen Prüfsumme - - - Tsd. Mio. Mrd. - Dateiname Fehler Datei existiert bereits Bitte warten… In Zwischenablage kopiert Bitte wähle ein verfügbares Downloadverzeichnis - Starten Pause Abspielen @@ -126,53 +109,41 @@ Threads NewPipe lädt herunter Für Details antippen - Ungültige URL oder Internet nicht verfügbar reCAPTCHA Schwarz - reCAPTCHA-Aufgabe reCAPTCHA-Aufgabe angefordert - Später - Ja Alle Kanal - Deaktiviert - Alten Player benutzen Im Pop-up-Modus öffnen Bevorzugtes Videoformat Spiele im Pop-up Modus ab NewPipe-Pop-up-Modus - - Diese Berechtigung ist für das Öffnen im Pop-up-Modus erforderlich - Alter eingebauter Mediaframework-Player Standardauflösung des Pop-ups Höhere Auflösungen anzeigen Nur manche Geräte unterstützen das Abspielen von 2K-/4K-Videos Hintergrund Pop-up - Größe und Position des Pop-ups merken -Entfernt Tonspur bei manchen Auflösungen + Entfernt Tonspur bei manchen Auflösungen Letzte Größe und Position des Pop-ups merken Gestensteuerung Helligkeit und Lautstärke mittels Gesten einstellen Suchvorschläge Beim Suchen Vorschläge anzeigen - Pop-up Filter Aktualisieren Löschen Größenänderung Beste Auflösung - Über Über NewPipe Website öffnen @@ -189,19 +160,16 @@ Drittanbieter-Lizenzen Auf GitHub ansehen Beitragen -Download + Download Erlaubte Zeichen im Dateinamen Ungültige Zeichen werden durch dieses Zeichen ersetzt Ersetzungszeichen - Buchstaben und Zahlen Abonnieren Abonniert Abonnement beendet Abos - Neuigkeiten - Suchverlauf Suchanfragen lokal speichern Verlauf & Cache @@ -213,56 +181,44 @@ Verlauf Der Verlauf ist leer Verlauf bereinigt - -Abonnement konnte nicht geändert werden + Abonnement konnte nicht geändert werden Abonnement konnte nicht aktualisiert werden - Nach Unterbrechungen (z.B. Telefonaten) Wiedergabe fortsetzen - - NewPipe-Benachrichtigung Benachrichtigungen für NewPipe-Hintergrund- und Pop-up Wiedergabe - Hauptmenü Verhalten Verlauf & Cache Wiedergabeliste Rückgängig machen - Keine Ergebnisse Keine Abonnenten - %s Abonnent - %s Abonnenten - - + %s Abonnent + %s Abonnenten + Keine Aufrufe - %s Aufruf - %s Aufrufe - - + %s Aufruf + %s Aufrufe + Keine Videos - Video - Videos - - + Video + Videos + Die meisten Sonderzeichen - Element gelöscht Fortsetzen bei erneutem Fokussieren Player Nichts hier außer das Zirpen der Grillen - Möchtest du dieses Element aus dem Suchverlauf löschen? -Leere Seite + Leere Seite Wähle einen Kanal aus Noch keine Kanalabonnements vorhanden Trends In der Warteschlange der Pop-up-Wiedergabe Alles abspielen - Entfernen Audio-Einstellungen Konnte diesen Stream nicht abspielen @@ -276,24 +232,21 @@ Top 50 Nicht behebbarer Wiedergabefehler aufgetreten Wiederherstellen nach einem Wiedergabefehler - Kiosk-Seite Kiosk auswählen - Kiosk Tipp anzeigen, wenn der Hintergrundwiedergabe- oder Pop-up-Button auf der Videodetailseite gedrückt gehalten wird In der Warteschlange der Hintergrundwiedergabe Neu & Heiß Halten, um zur Wiedergabeliste hinzuzufügen -\"Gedrückt halten, um hinzuzufügen\" Tipp anzeigen + \"Gedrückt halten, um hinzuzufügen\" Tipp anzeigen [Unbekannt] - In Warteschlange für Hintergrundwiedergabe In Warteschlange für Pop-up Ab hier wiedergeben Ab hier im Hintergrundmodus Ab hier im Pop-up -Spenden + Spenden Zurückgeben Website Besuche die NewPipe Website für weitere Informationen und Neuigkeiten. @@ -303,71 +256,54 @@ Bevorzugtes Land des Inhalts Immer Nur einmal - Ausrichtung umschalten In den Hintergrund wechseln Zum Pop-up wechseln Zur normalen Wiedergabe wechseln - Externe Player unterstützen diese Art von Links nicht Ungültige URL Keine Video-Streams gefunden Keine Audio-Streams gefunden - Navigationsleiste öffnen Navigationsleiste schließen - Video-Player Hintergrund-Player Popup-Player Immer fragen - Informationen werden abgerufen… Gewünschten Inhalt laden -Datenbank importieren + Datenbank importieren Datenbank exportieren Überschreibt deinen aktuellen Verlauf und deine Abonnements Verlauf, Abonnements und Wiedergabelisten exportieren Keine gültige ZIP-Datei Warnung: Nicht alle Dateien konnten importiert werden. Dies wird deine aktuellen Einstellungen überschreiben. - Info anzeigen - Lesezeichen für Wiedergabelisten - Hinzufügen zu - Zum Neuordnen ziehen - Erstellen Einen löschen Alle löschen Umbenennen - Möchtest du dieses Element aus dem Wiedergabeverlauf löschen? Bist du sicher, dass du alle Elemente aus dem Verlauf löschen möchtest? Zuletzt wiedergegeben Am häufigsten wiedergegeben - Immer fragen - Neue Wiedergabeliste Löschen Umbenennen Zur Wiedergabeliste hinzufügen Als Vorschaubild der Wiedergabeliste festlegen - Lesezeichen entfernen - Diese Wiedergabeliste löschen? Wiedergabeliste erstellt Zur Wiedergabeliste hinzugefügt Vorschaubild der Wiedergabeliste geändert. Konnte Wiedergabeliste nicht löschen. - Keine Untertitel - Schriftgröße der Untertitel Abbrechen Normale Schriftgröße @@ -375,30 +311,24 @@ Schnelle, ungenaue Suche verwenden Mit ungenauem Suchen kann die Abspielposition schneller erreicht werden, aber auf Kosten der Genauigkeit Datei - Verzeichnis existiert nicht Die Datei existiert nicht oder die Rechte zum Lesen oder Schreiben fehlen Dateiname darf nicht leer sein Ein Fehler ist aufgetreten: %1$s - Automatisch erzeugt Kleinere Schrift Größere Schrift - LeakCanary aktivieren Import von Export nach - Importiere… Exportiere… - Datei importieren Vorheriger Export - Beachte, dass diese Aktion das Netzwerk stark belasten kann. \n \nMöchtest du fortfahren? -Vorschaubilder laden + Vorschaubilder laden Bilder-Cache gelöscht Zwischengespeicherte Metadaten löschen Alle zwischengespeicherten Website-Daten entfernen @@ -412,29 +342,23 @@ Import Abonnements konnten nicht importiert werden Abonnements konnten nicht exportiert werden - Wiedergabegeschwindigkeitsregler Geschwindigkeit Tonhöhe Verknüpfung aufheben (kann zu Verzerrungen führen) Nightcore Standard -Abschalten, um das Laden von Miniaturansichten zu verhindern, was Daten- und Speicherverbrauch spart. Änderungen löschen den Bildzwischenspeicher sowohl im Arbeitsspeicher als auch auf der Festplatte. + Abschalten, um das Laden von Miniaturansichten zu verhindern, was Daten- und Speicherverbrauch spart. Änderungen löschen den Bildzwischenspeicher sowohl im Arbeitsspeicher als auch auf der Festplatte. Nächsten Stream automatisch einreihen Automatisches Anhängen eines verwandten Streams beim Abspielen des letzten Streams in einer nicht wiederholten Warteschlange. Hier wird bald etwas stehen ;D - - Wiedergabeliste mit Lesezeichen versehen Anpassen Füllen Vergrößern - Speicherlecküberwachung kann dazu führen, dass die App beim Heap-Dumping nicht mehr reagiert - Fehler außerhalb des Lebenszyklus melden Erzwingen der Meldung unzustellbarer Rx-Ausnahmen außerhalb des Lebenszyklus von Fragmenten oder Aktivitäten nach der Entsorgung - Importiere YouTube-Abonnements, indem du die Exportdatei herunterlädst: \n \n1. Gehe zu dieser URL: %1$s @@ -447,17 +371,12 @@ \n3. Melden dich an, falls du dazu aufgefordert wirst \n4. Kopiere die Profil-URL, zu der du weitergeleitet wurdest. yourID, soundcloud.com/yourid - Keine Streams zum Download verfügbar - Bevorzugte \'Öffnen\' Aktion Standardaktion beim Öffnen von Inhalten - %s - Untertitel Textgröße und Hintergrund der Untertitel im Player anpassen. Wird erst nach Neustart der App wirksam. - Keine App zum Abspielen dieser Datei installiert - Wiedergabeverlauf löschen Löscht den Verlauf der abgespielten Streams Den ganzen Wiedergabeverlauf löschen\? @@ -467,17 +386,15 @@ Den gesamten Suchverlauf löschen\? Suchverlauf gelöscht. 1 Element gelöscht. - NewPipe ist freie Copyleft-Software: Du kannst sie nach Belieben benutzen, untersuchen, mit anderen teilen und verbessern. Insbesondere kannst du sie unter den von der Free Software Foundation veröffentlichten Bedingungen der GNU General Public License, in der Version 3 der Lizenz oder (nach deiner Wahl) jeder späteren Version, weitergeben und/oder verändern. Möchtest du auch Einstellungen importieren? - NewPipe-Datenschutzbestimmungen Dem NewPipe-Projekt ist Datenschutz sehr wichtig. Deshalb sammelt diese App keine Daten ohne deine Zustimmung. \nNewPipes Datenschutzbestimmungen erklären im Detail, welche Daten beim Absenden eines Absturzberichtes verschickt und gespeichert werden. Datenschutzbestimmungen lesen Akzeptieren Ablehnen -Um der europäischen Datenschutz-Grundverordnung (DSGVO) gerecht zu werden, weisen wir hiermit auf NewPipe\'s Datenschutzerklärung hin. Bitte lies sie sorgfältig durch. + Um der europäischen Datenschutz-Grundverordnung (DSGVO) gerecht zu werden, weisen wir hiermit auf NewPipe\'s Datenschutzerklärung hin. Bitte lies sie sorgfältig durch. \nDu musst den Datenschutzrichtlinien zustimmen, um den Fehlerbericht an uns zu senden. Unbegrenzt Auflösung bei Verwendung mobiler Daten begrenzen @@ -486,11 +403,9 @@ Keine Zum Hintergrund-Player minimieren Zum Popup-Player minimieren - -Vorspulen während der Stille + Vorspulen während der Stille Schritt Zurücksetzen - Kanäle Wiedergabelisten Titel From b7667ce97a5f78c3cce019e73031e82ed055fbaa Mon Sep 17 00:00:00 2001 From: Nathan Follens Date: Sun, 10 Mar 2019 15:32:07 +0000 Subject: [PATCH 017/138] Translated using Weblate (Flemish) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-nl-rBE/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-nl-rBE/strings.xml b/app/src/main/res/values-nl-rBE/strings.xml index 6eff6b040..5fb175b3e 100644 --- a/app/src/main/res/values-nl-rBE/strings.xml +++ b/app/src/main/res/values-nl-rBE/strings.xml @@ -292,7 +292,7 @@ Opgelet: kon niet alle bestanden importeren. Dit zal uw huidige configuratie overschrijven. Kiosk - Trending + Populair Top 50 Nieuw en populair Achtergrondspeler From fc4e007cc4879a01f477326ac4832116892be53e Mon Sep 17 00:00:00 2001 From: WaldiS Date: Sat, 9 Mar 2019 16:47:42 +0000 Subject: [PATCH 018/138] Translated using Weblate (Polish) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-pl/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 27a398e60..cc4e717c7 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -208,7 +208,7 @@ %s subskrybent %s subskrybentów - %s subskrybentów + %s subskrybenci %s odtworzenie @@ -356,7 +356,7 @@ \n \nCzy chcesz kontynuować? Kontrola prędkości odtwarzania - Tempo + Czas Wysokość dźwięku twojeID, soundcloud.com/yourid Odłącz (może powodować zniekształcenia) From 70238fd77300f2a811644f0ac437c3353a99e39e Mon Sep 17 00:00:00 2001 From: Olexandr Nesterenko Date: Sun, 10 Mar 2019 13:27:35 +0000 Subject: [PATCH 019/138] Translated using Weblate (Ukrainian) Currently translated at 98.4% (436 of 443 strings) --- app/src/main/res/values-uk/strings.xml | 168 +++++++++++-------------- 1 file changed, 76 insertions(+), 92 deletions(-) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index eaca5719a..81e25e7be 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -16,12 +16,9 @@ обертання Зовнішній відео-програвач Зовнішній авдіопрогравач - Шлях для завантаження відеозапису Вкажіть шлях для завантаження відеозаписів - Вкажіть шлях для завантаження авдіофайлів - Шлях де зберігатимуться завантажені відеозаписи Тека для завантаженого аудіо Завантажені авдіофайли зберігатимуться тут @@ -37,7 +34,6 @@ Тема Темна Світла - Завантажити Наступний відеозапис Являти \"наступні\" й \"схожі\" відео @@ -51,9 +47,8 @@ У доступі відмовлено Контент Контент з віковими обмеженнями - Показувати відеозаписи, які мають вікові обмеження. Спершу активуйте опцію для програвання таких записів у \"налаштуваннях\". + Показувати відеозаписи, які мають вікові обмеження. Спершу активуйте опцію для програвання таких записів у налаштуваннях. НАЖИВО - Помилка Помилка мережі Не вдалося завантажити всі ескізи @@ -73,7 +68,6 @@ Що сталося: Натисніть на пошук аби почати Чорна - Завантаження Завантаження Звіт про помилку @@ -82,13 +76,10 @@ Так Пізніше Вимкнено - Не вдалося завантажити зображення Застосунок/інтерфейс зазнав краху Ваш коментар (англійською): Деталі: - - Ескіз попереднього перегляду відео Ескіз попереднього перегляду відео Використовувати Tor @@ -96,7 +87,6 @@ Повідомити про помилку Не вдалося створити теку для завантаження \'%1$s\' Створити теку для завантаження \'%1$s\' - Відео Аудіо Повторити @@ -104,13 +94,11 @@ т млн мрд - Почати Павза Відтворити Видалити Контрольна сума - ОК Назва файлу Гілки @@ -122,8 +110,7 @@ Будь ласка, зачекайте… Копійовано до сховку Будь ласка, оберіть теку для завантаження - -Потокового програвача не знайдено (ви можете встановити VLC для відтворення). + Потокового програвача не знайдено (ви можете встановити VLC для відтворення). Відкрити у віконному режимі Усувається звук ПЕВНИХ роздільностей NewPipe у віконному режимі @@ -132,15 +119,11 @@ Ви відписалися від каналу Неможливо змінити підписання Неможливо оновити підписання - Головна Підписання - Новинки - На тлі У вікні - Типова роздільна здатність вікна Не всі пристрої підтримують відтворення 2K/4K відеозаписи Вищі роздільні здатності @@ -177,16 +160,12 @@ Грати всі Завжди Тільки тепер - NewPipe сповіщення "Сповіщення для NewPipe-ових тлового і віконного програвачів " - [Невідомо] - Перемкнутися до тла Перемкнутися до вікна Перемкнутися на головну - Імпортувати базу Експортувати базу Перепише поточну історію та підписки @@ -196,23 +175,19 @@ Помилкова URL Відеостримів не знайдено Не знайдено аудіопотоків - Звіт користувача Без підписників Немає відео Помилкова URL або інтернет недоступний Цей дозвіл має бути відкритим \nу вікні - «reCAPTCHA» Завантаження Допустимі символи у назвах файлів "Неправильні символи коригуватимуться цим " Символ заміни - Літери та цифри Більш специфічні символи - Про NewPipe Налаштування Візитівка @@ -230,20 +205,15 @@ Новинки Деталі Звукові налаштування - Відеопрогравач Тловий програвач Віконний програвач - Отримую інформацію… Завантажується запитаний контент -Завантажити потоковий файл + Завантажити потоковий файл Показати інформацію - Закладкові плейлисти - Додати до - Показувати підказку коли натиснута кнопка тла або вікна на сторінці інформації відеозапису Перемкнути орієнтацію Фатальна помилка програвача @@ -254,44 +224,37 @@ Не сподобалося Нічого не знайдено Перетягніть для зміни сортування - - %s підписник - %s підписники - n > 1 - - + %s підписник + %s підписники + n > 1 + - %s відеозапис - %s відеозаписи - %s відеозаписів - - + Відеозапис + Відеозаписи + Відеозаписів + Створити Видалення одного Видалити всі Відхилити Змінити назву - Сторонні ліцензії Перейти до сайту Візитівка Вільний та легковагий стримінґ для Андроїду. Які б не були Ваші ідеї: переклад, дизайн, легкий чи глобальний рефакторинґ - будь-яка допомога завжди у нагоді. Що більше зроблено, то ліпшим NewPipe стає! -Усунення вад + Усунення вад Нічого нема - Перегляди відсутні - %s перегляд - %s переглядів - %s переглядів - - + %s перегляд + %s перегляди + %s переглядів + Нове завдання Виклик reCAPTCHA Запит на виклик reCAPTCHA - © %1$s by %2$s under %3$s Учасники NewPipe розроблений добровольцями, які витрачають власний час заради вашого задоволення. Допоможіть розробникам зробити NewPipe ще ліпшим, насолоджуючись філіжанкою кави. @@ -299,8 +262,6 @@ Завітайте на офіційний сайт, аби отримати більше інформацій та новин. Ліцензія NewPipe Прочитати ліцензію - - Історія пошуку Переглянуто Історію вимкнено @@ -312,7 +273,6 @@ Чи ви впевнені, що хочете видали всі елементи з історії\? Програвалося останнім Програвалося найбільше - Контент на головній сторінці Порожня сторінка Kiosk @@ -325,7 +285,6 @@ Недійсний ZIP-файл Увага: не можливо здійснити імпортування всіх файлів. Це перепише ваші поточні налаштування. - Кіоски Набуває популярності Тловий програвач @@ -337,71 +296,52 @@ Розпочати відтворення звідси Розпочати звідси у тловому програвачеві Розпочати відтворення у вікні звідси - Відкрити шухляду Закрити шухляду Незабаром тут щось буде ;D - - Завжди питати - Новий плейлист Видалити Змінити назву Назва Додати до плейлиста Установити як ескіз плейлиста - Додати плейлист до закладок Усунути закладку - Чи видалити цього плейлиста\? Плейлист було створено Додано до плейлиста Ескіз плейлиста змінився. Неможливо видалити плейлист. - Субтитри відсутні - Замостити Заповнити Збільшити - Створено автоматичним шляхом - Увімкнути LeakCanary Моніторування витіків пам\'яті може призвести до нереагівності застосунку - Зазвітувати out-of-lifecycle хиби Примусове звітування про неможливість доставлення Rx винятків, які відбуваються за межами фраґменту або діяльності життєвого циклу після усунення - -Використовувати неточний пошук + Використовувати неточний пошук Неточний пошук дозволяє програвачеві рухатися позиціями швидше, проте з меншою точністю Додавати в чергу наступний стрим Автоматично додавати пов\'язаний стрим під час відтворення останнього у черзі без повторювань. - Файл - Такої теки не існує Не існує файлу/джерела контенту Файл не існує, або немає дозволу на його запис або читання Назва файлу не може бути порожньою Помилка: %1$s - Імпортування/експортування Імпортування Імпортувати з Експортувати до - Імпортування… Експортування… - Імпортування файлу Попереднього експортування - Неможливо імпортувати підписання Неможливо експортувати підписання - Імпортуйте Ютюбові підписання завантаженням файлу еспортування: \n \n1. Перейдіть за цією ланкою: %1$s @@ -414,12 +354,11 @@ \n3. За запитом увійдіть до вашої обліківки \n4. Скопіюйте URL профайлу, до якого вас переспрямує. вашID, soundcloud.com/вашid - Майте на увазі: ця операція може потребувати багато трафіку. \n \nПродовжуватимете? -Завантажити ескізи - При вимкненому завантаженні ескізів, заощадується трафік та внутрішня пам\'ять. Зміни призведуть очищення кешу зображень. + Завантажити ескізи + Вимкніть завантаженні ескізів, заощаджується трафік та внутрішня пам\'ять. Зміни призведуть очищення кешу зображень. Кеш зображень стерто Стерти кеш метаданих "Усунути всі кешовані дані вебсторінки " @@ -428,16 +367,12 @@ Темп Тон Від\'єднати (може спричинити спотворення) -Стрими недоступні для завантаження - + Стрими недоступні для завантаження Переважний спосіб \"відкриття\" Типова дія під час відкриття вмісту — %s - Субтитри Змінення маштабу субтитрового тексту та тлових стилів. Увімкнення функції потребує перезавантаження застосунку. - Не знайдено відповідного застосунку для відтворення цього файлу - Очистити історію переглядів Видаляє історію відтворень Чи видалити всю історію переглядів\? @@ -447,10 +382,8 @@ Чи видалити всю пошукову історію\? Пошукову історію видалено. Видалено один фрагмент. - "NewPipe є вільною копілефт програмою: Ви можете використовувати її, поширювати та вдосконалювати з розсудом. Зокрема ви можете перерозподіляти та/або змінювати її за умов використання GNU General Public License, опублікованою Free Software Foundation, під 3-ю версією ліцензії, або (на ваш вибір) будь-якою пізнішою версією." Бажаєте імпортувати з налаштуваннями\? - NewPipe\'ова політика приватності Проєкт NewPipe дуже серйозно ставиться до вашої приватності. Тому застосунок не збирає ніяких даних без вашої згоди \nПолітика приватності NewPipe пояснює у деталях, які дані було відіслано та збережено, під час відсилання крахового звіту. @@ -459,18 +392,16 @@ \nВи маєте підтвердити її аби надіслати нам баґового звіту. Ухвалити Відхилити -Безмежно + Безмежно Обмеження якості відео (мобільний трафік) Перемотувати підчас тиші Крок Скинути - Зменшити при перемкненні застосунку Дія при перемкненні до іншого застосунку з головного відеопрогравача — %s Жодних До тлового програвача Зменшити до віконного програвачу - Канали Плейлисти Стежки @@ -478,5 +409,58 @@ Вигляд списку Список Сiтка - - + Відписатися + Нова вкладка + Обрати вкладку + Жест керування гучністю + Змінювати гучність плеєра жестами + Жест регулювання яскравістю + Змінювати яскравість жестами + Оновлення + Події + Файл видалено + Сповіщення про оновлення + Сповіщення про нову версію NewPipe + Зовнішнє сховище недоступне + Відновити на типові + Бажаєте відновити на типові\? + Кількість підписників недоступне + Вибір + Конференції + Оновлення + Автоматично + Перемкнути вигляд + Доступна нова версія NewPipe! + Натисніть для завантаження + Завершено + В черзі + призупинено + додано в чергу + пост-обробка + Черга + Дію заборонено системою + Помилка завантаження + Завантаження завершено + Завантажень завершено: %s + Створення унікального імені + Перезаписати + Файл з таким іменем вже існує + Файл з таким іменем вже завантажується + Показати помилку + Код + Не можливо створити файл + Не можливо створити теку призначення + Не надано доступу системою + Захищене з\'єднання не встановлено + Сервер не знайдено + Не вдалося з\'єднатися із сервером + Не вдалося отримати дані з серверу + Сервер не підтримує завантаження на декілька потоків, спробуйте з параметром @string/msg_threads = 1 + Не знайдено + Невдала пост-обробка + Очистити завершені завантаження + Зупинити + Максимум спроб + Максимальна кількість спроб перед скасуванням завантаження + Призупиняти завантаження при переході на стільникові дані + \ No newline at end of file From ffa4b1483fab10a4fb5dcc30c436e1c0327f3762 Mon Sep 17 00:00:00 2001 From: Der_Floh Date: Sun, 10 Mar 2019 19:11:37 +0000 Subject: [PATCH 020/138] Translated using Weblate (German) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-de/strings.xml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 319e5e8e6..eb891dec0 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -2,9 +2,11 @@ %1$s Aufrufe Veröffentlicht am %1$s - Keinen Streamplayer gefunden. Möchtest du VLC installieren? + Keinen Streamplayer gefunden. Willst du VLC installieren\? +\n\t Installieren - Abbrechen + Abbrechen +\n Im Browser öffnen Teilen Download @@ -78,8 +80,8 @@ Entschuldigung. Es sind einige Fehler aufgetreten. Dein Kommentar (auf englisch): Konnte keinen Stream abrufen - Autoplay - Spiele ein Video ab, wenn NewPipe von einer anderen App aufgerufen wurde + Automatische Wiedergabe + Spielt ein Video ab, wenn NewPipe von einer anderen App aufgerufen wurde Einen Fehler melden Anwenderbericht LIVE From 716f7e722b2c2373c461bc74c4aedfe54ce1ce22 Mon Sep 17 00:00:00 2001 From: dextro67 Date: Mon, 11 Mar 2019 02:11:11 +0000 Subject: [PATCH 021/138] Translated using Weblate (Hindi) Currently translated at 73.6% (326 of 443 strings) --- app/src/main/res/values-hi/strings.xml | 94 +++++--------------------- 1 file changed, 16 insertions(+), 78 deletions(-) diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index d78e37403..4d7397152 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -1,6 +1,6 @@ -%1$s दृश्य + %1$s दृश्य %1$s को प्रकाशित कोई स्ट्रीम प्लेयर नहीं मिला। क्या आप VLC इंस्टॉल करना चाहते हैं? इंस्टॉल करें @@ -17,10 +17,8 @@ सदस्यता ली चैनल सदस्यता रद्द करी गयी सदस्यता - पीछे पॉपअप - ऑटोप्‍ले करें ऑडियो हलका @@ -44,7 +42,6 @@ न्यूपाइप का लाइसेंस लाइसेंस पढ़ें योगदान करें - इतिहास खोजा जा चूका है विडियो जो देख लिए गए है @@ -63,19 +60,15 @@ कुछ रेसोल्युशनस में नहीं आएगी अन्य ऑडियो प्लेयर उपयोग करें मुख्य - सदस्यता को बदलने में असमर्थ है + "सदस्यता को बदलने में असमर्थ " सदस्यता को अपडेट करने में असमर्थ है - देखे की क्या नया है - विडियो को डाउनलोड करने के लिए फाइल की जगह डाउनलोड किए गए विडियो फाइल को रखने की जगह वीडियो के लिए डाउनलोड पथ दर्ज करें - ऑडियो डाउनलोड फ़ोल्डर डाउनलोड किये गए ऑडियो यहाँ है ऑडियो फाइल डाउनलोड करने के लिए जगह दर्ज करें - अन्य अप्प के द्वारा NewPipe के आह्वान पर वीडियो तुरंत चले वीडियो का डिफ़ॉल्ट रिज़ॉल्यूशन विडियो पॉपअप का डिफ़ॉल्ट रिज़ॉल्यूशन @@ -107,7 +100,6 @@ जब बैकग्राउंड और पॉपअप बटन विडियो के विवरण पन्ने में दबाई जाए तो tip को दिखाए ये वाला URL इसमें नहीं चलेगा डिफ़ॉल्ट विषय की भाषा - प्लेयर चाल चलन पॉपअप @@ -137,12 +129,9 @@ बेहतर विडियो की क्वालिटी वापस जाए सारे प्ले करे - NewPipe की अधिसूचना NewPipe के बैकग्राउंड में चल रहे विडियो और पॉपअप विडियो के लिए अधिसूचना - [नहीं जानते] - त्रुटी नेटवर्क में त्रुटी सारे thumbnail(फोटो जो फ़ोन की मेमोरी में है ) भरे नहीं जा सकते @@ -159,7 +148,6 @@ इस विडियो को चलाने में असफल हुए कभी ठीक न होने वाले विडियो प्लेयर की त्रुटी आ रही है विडियो प्लेयर त्रुटी से ठीक हो रहा है - खेद है की, ऐसा होना नहीं चाहिए था. त्रुटी की रिपोर्ट को ईमेल से भेजे माफ़ करे , कुछ त्रुटियाँ हो रही है @@ -169,8 +157,6 @@ क्या:\\nमांग:\\nविषयवस्तु की भाषा:\\nसेवा:\\nजीएमटी समय:\\nपैकेज:\\nसंस्करण:\\nOS संस्करण: आपकी टिप्पणी: विवरण: - - विडियो के thumbnail के पूर्व दर्शन विडियो के thumbnail के पूर्व दर्शन अपलोडर के thumbnail वाले फोटो @@ -182,46 +168,37 @@ उपयोगकर्ता की रिपोर्ट कोई परिणाम नहीं मिला यंहा कुछ नहीं है - "डाउनलोड वाली डायरेक्टरी को बना नहीं सकते \'%1$s\'" डाउनलोड की डायरेक्टरी बना दी गई है \'%1$s\' - विडियो ऑडियो फिर से कोशिश करे स्टोरेज में प्रवेश के लिए अनुमति नहीं मिली पुराना विडियो प्लेयर प्रयोग करे पुराना Mediaframework Player जो फ़ोन में बना हुआ है - हज़ार करोड़ अरब - कोई भी सदस्य नहीं है - %s सदस्य - %s सदस्यो - - + %s सदस्य + %s सदस्यो + - %s दर्शक - %s दर्शके - - + %s दर्शक + %s दर्शके + - %s विडियो - %s वीडियो - - + %s विडियो + %s वीडियो + शुरू रोके चलाये मिटाए checksum - नया मिशन ठीक है - फाइल का नाम मेसेज के thread त्रुटी @@ -234,26 +211,21 @@ क्लिपबोर्ड पर कॉपी हो गया है कृपया उपलब्ध डाउनलोड फोल्डर को चुने पॉपअप के तरीके में खोलने के लिए अनुमति की जरुरत है - reCAPTCHA reCAPTCHA चुनोती reCAPTCHA चुनोती के लिए निवेदन - डाउनलोड फाइल के नाम के लिए आवश्यक characters(जैसे - १२३, abc) की अनुमति है अमान्य characters इस value से बदल जायेंगे रिप्लेसमेंट करैक्टर - वर्ण और अंक विशेष वर्ण - %2$s के द्वारा © %1$s जो %3$s के अधीन आते है लाइसेंस load नहीं हो रहा सहयोगकर्ता एंड्राइड के लिए हल्का और मुफ्त स्ट्रीमिंग एप्लिकेशन| अगर आपके पास कोई सुझाव हो जैसे -अनुवाद , डिजाईन में बदलाव ,code को साफ़ रखना , या फिर code में जायदा बदलाव लाना हो तो - साहयता के लिए आपका स्वागत है . जितना ज्यादा होगा उतना बेहतर होगा ! क्या आप इसको खोज इतिहास के मिटाना चाहते है ? - मुख्य पेज की विषयवस्तु खाली पन्ना kiosk पन्ना @@ -263,7 +235,6 @@ चैनल को चुने अभी तक किसी भी चैनल के सदस्य नहीं है kiosk को चुने - kiosk टॉप 50 नया और गरम @@ -277,24 +248,19 @@ यंहा से चलाना शुरू करे बैकग्राउंड में चलाना शुरू करे पॉपअप में चलाना शुरू करे -स्ट्रीम करने के लिए प्लेयर उपलब्ध नहीं है (आप इसे चलाने के लिए VLC प्लेयर इंस्टॉल कर सकते हैं)। + स्ट्रीम करने के लिए प्लेयर उपलब्ध नहीं है (आप इसे चलाने के लिए VLC प्लेयर इंस्टॉल कर सकते हैं)। स्ट्रीम डाउनलोड करें जानकारी दिखाएं - बुकमार्क किये गए प्लेलिस्टस - में जोड़े - डिफ़ॉल्ट देश का विषय सर्विस हमेशा के लिए सिर्फ एक बार के लिए - टॉगल ओरिएंटेशन बैकग्राउंड में स्विच करें पॉपअप मोड में जाएं मुख्य पर स्विच करें - डेटाबेस आयात करें डेटाबेस निर्यात करें आपके वर्तमान इतिहास और सब्सक्रिप्शन को ओवरराइड करेगा @@ -303,15 +269,12 @@ अमान्य URL कोई वीडियो स्ट्रीम नहीं मिला कोई वीडियो स्ट्रीम नहीं मिला - फिर से क्रम देने के लिए खींचें - बनाइये एक को हटाएं सभी को हटाएँ ख़ारिज करें नाम बदलें - दान करें NewPipe स्वयंसेवकों द्वारा विकसित किया जाता है जो आपको अच्छा अनुभव देने के लिए अपना खाली समय व्यतीत करते हैं। स्वयंसेवको को मदद भेजे, ताकि वह NewPipe को और अच्छा बना सके। वापस दे @@ -321,61 +284,48 @@ क्या आप वाकई इतिहास से सभी आइटंस हटाना चाहते हैं? पिछला चलाया गया अधिकतम चलाए गए - निर्यात पूर्ण हुआ आयात पूर्ण हुआ कोई मांय ज़िप फ़ाइल नहीं चेतावनी: सभी फ़ाइलों को आयात नहीं किया जा सका। यह आपके वर्तमान सेटअप को ओवरराइड करेगा । - ड्रावर खोलें ड्रावर बंद करें जल्द ही यहां पर कुछ दिखाई देगा ;D - - वीडियो प्लेयर बैकग्राउंड प्लेयर पॉपअप प्लेयर हमेशा पूछें - जानकारी प्राप्त की जा रही है… अनुरोधित सामग्री लोड कर रहे है - नई प्लेलिस्ट बनाएं प्लेलिस्ट हटाएं प्लेलिस्ट का नाम बदलें नाम प्लेलिस्ट में जोड़ें प्लेलिस्ट थंबनेल के रूप में सेट करें - प्लेलिस्ट बुकमार्क करें बुकमार्क हटायें - क्या आप इस प्लेलिस्ट को हटाना चाहते हैं? सूची बना दी गई प्लेलिस्ट में जोड़ा गया प्लेलिस्ट का थंबनेल बदल दिया गया है सूची हटाने में असफल - कोई कैप्शन नहीं है - फिट भरें ज़ूम करें - कैप्शन फ़ॉंट आकार छोटे फ़ॉंट सामांय फ़ॉंट बड़ा फ़ॉंट - मॉनिटर लीक मेमोरी लीक मॉनिटरिंग सक्षम है, हीप डंपिंग के समय ऐप अप्रतिसादी हो सकती है मेमोरी लीक मॉनिटरिंग अक्षम है -डीबग करें + डीबग करें ऑटो-जनरेटेड LeakCanary सक्षम करें हीप डंप करने के दौरान मेमोरी लीक मॉनिटरिंग ऐप को अनुत्तरदायी बना सकता है - Out-of-Lifecycle त्रुटियों की रिपोर्ट करें छायाप्रारुप लोड करें तेजी से अचूक तलाश का प्रयोग करें @@ -388,7 +338,6 @@ अगली स्ट्रीम को स्वचालित रूप से जोड़ें गैर-दोहराने वाली कतार में अंतिम स्ट्रीम चलाते समय संबंधित स्ट्रीम को स्वतः संलग्न करें फाइल - चेनल्स सूची ट्रेक @@ -407,48 +356,36 @@ फ़ाइल का नाम खाली नहीं हो सकता एक भूल हुई: %1$s डाउनलोड करने के लिए कोई स्ट्रीम उपलब्ध नही है - एक चीज़ साफ कर दी गई। - इस फ़ाइल को चलाने के लिए कोई ऐप स्थापित नही है - न्यूपाइप की गोपनीयता नीति न्यूपाइप परियोजना आपकी गोपनीयता को बहोत गंभीर रूप से लेता है। इसलिए, ऐप आपकी अनुमति के बिना कोई डेटा जमा नही करता। \nन्यूपाइप की गोपनीयता नीति विस्तार से समज़ाती है कि कोनसा डेटा भेजा या संग्रह किया जाता है जब आप क्रेश विवरण भेजते है। गोपनीयता नीति पढ़े क्या आप सेटिंग्स भी आयात करना चाहते है? - पसंदीदा \'खोलने पर\' करवाई सामग्री खोलते समय डिफ़ॉल्ट कारवाही — %s - केप्सन प्लेयर केप्शन के शब्दों का परिमाण और पृष्ठभूमि शैलियो को बदले। लागू करने के लिए ऐप को पुनः प्रारम्भ करना जरूरी है। - आयात/निर्यात आयात से आयात करे पर निर्यात करे - आयात किया जा रहा है… निर्यात किया जा रहा है… - फाइल आयात करे पहले वाला निर्यात - सब्सक्रिप्शन आयात नही कर सके सब्सक्रिप्शन निर्यात नही कर सके - निर्यात की गई फाइल को डाउनलोड करके यूट्यूब सब्सक्रिप्शन को आयात करे: \n \n1. इस URL पर जाए: %1$s \n2. जब कहा जाए, लॉगिन करे \n3. एक डाउनलोड शुरू होना चाहिए (यही निर्यात की गई फाइल है) आपका आई डी, soundcloud.com/yourid - ध्यान रखे, यह तरीका नेटवर्क साधनो के लिए मंहगा हो सकता है। \n \nक्या आप आगे बढ़ना चाहते है? - चलाने की गति के नियंत्रण गति ऊंचाई @@ -456,10 +393,8 @@ खामोशी के समय तेज़ी से आगे बढ़े कदम रिसेट - स्वीकारे अस्वीकार करे - असीमित मोबाइल डेटा उपयोग करते समय रेसॉल्युसेन मर्यादित करे ऐप बदलते समय मिनिमाइज करे @@ -468,4 +403,7 @@ पृष्ठभूमि प्लेयर जैसे मिनिमाइज करे पॉप अप प्लेयर जैसे मिनिमाइज करे न्यूपाइप एक काॅपीलेफ़्ट फ़्री साॅफ़्टवेर है: इसे आप अपनी इच्छा के अनुसार इस्तेमाल, जाँच, बाँट तथा और बेहतर बना सकते है। खास तौर पर आप इसे फ़्री साॅफ़्टवेर फ़ाउंडेशन के द्वारा जारी जीएनयू जनरल पब्लिक लाइसेंस के तीसरे या उसके बाद आने वाले कोई भी वर्णन के शर्तों के मुताबिक फिर से बाँट या बदल सकते हैं। + सदस्यता वापस ले ली + नया टॅब + टॅब चुने \ No newline at end of file From d8746dc5926aab25fa56095f2f5f4ee71b6cc2ba Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Thu, 14 Mar 2019 09:20:37 +0100 Subject: [PATCH 022/138] hotfix decrypt function and move on to version v0.16.1 --- app/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 28a4861f4..3b1b8b4f9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,8 +8,8 @@ android { applicationId "org.schabi.newpipe" minSdkVersion 19 targetSdkVersion 28 - versionCode 720 - versionName "0.16.0" + versionCode 730 + versionName "0.16.1" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true @@ -57,7 +57,7 @@ dependencies { exclude module: 'support-annotations' }) - implementation 'com.github.TeamNewPipe:NewPipeExtractor:8de53111d9' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:e072bf6461b295' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:2.23.0' From 48f966e7db3e240622fd118270c32c7237b51938 Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Thu, 14 Mar 2019 09:24:02 +0100 Subject: [PATCH 023/138] add changelog for 730 --- fastlane/metadata/android/en-US/changelogs/730.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/en-US/changelogs/730.txt diff --git a/fastlane/metadata/android/en-US/changelogs/730.txt b/fastlane/metadata/android/en-US/changelogs/730.txt new file mode 100644 index 000000000..c317f53d2 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/730.txt @@ -0,0 +1,2 @@ +# Fixed +- Hot fix decrypt function error again. From 8b86f9ea6d75a7f0f9b7fa996d92096503426cde Mon Sep 17 00:00:00 2001 From: Gontzal Manuel Pujana Onaindia Date: Thu, 14 Mar 2019 16:35:07 +0000 Subject: [PATCH 024/138] Translated using Weblate (Basque) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-eu/strings.xml | 105 ++++--------------------- 1 file changed, 16 insertions(+), 89 deletions(-) diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index f6c723bd4..d49af0961 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -29,7 +29,6 @@ Edukiaren hizkuntz lehenetsia Bideoa eta Audioa Erreproduzitu - Bideoaren aurreikuspen argazkitxoa Bideoaren aurreikuspen argazkitxoa Igotzailearen abatarraren iruditxoa @@ -45,20 +44,17 @@ Ukitu bilaketa hasteko Audioa deskargatzeko karpeta Zehaztu audioa deskargatzeko bide-izena - Deskargatutako audioa hemen gordetzen da Erreprodukzio automatikoa Bideoa abiatzen du NewPipe beste aplikazio batek deitu badu Iluna Argia - Itxura -Ireki laster-leiho moduan + Ireki laster-leiho moduan Audioa kentzen du bereizmen batzuetan NewPipe laster-leiho modua Bigarren planoa Laster-leihoa - Laster-leihoaren lehenetsitako bereizmena Erakutsi bereizmen altuagoak Gailu batzuk besterik ez dute onartzen 2K/4K bideoak erreproduzitzea @@ -71,7 +67,6 @@ Erabili keinuak erreproduzigailuaren distira eta bolumena kontrolatzeko Bilaketa-iradokizunak Erakutsi iradokizunak bilatzean - Laster-leihoa Besteak Laster-leiho moduan erreproduzitzen @@ -92,7 +87,6 @@ Garbitu Tamainaz aldatzen Bereizmen onena - Errorea Sare-errorea Ezin izan dira iruditxo guztiak deskargatu @@ -115,35 +109,26 @@ Zer:\\nEskaria:\\nEdukiaren hizkuntza:\\nZerbitzua:\\nGMT Ordua:\\nPaketea:\\nBertsioa:\\nSE bertsioa: Zure iruzkina (Ingelesez): Xehetasunak: - - Eman errore baten berri Erabiltzaile-txostena - Ezin izan da \'%1$s\' karpeta sortu deskargetarako \'%1$s\' karpeta sortu da deskargetarako - Bideoa Audioa Saiatu berriro Biltegia atzitzeko baimena ukatu da Erabili erreproduzigailu zaharra Barne Media Framework erreproduzigailu zaharra - - K M MM - Hasi Pausatu Jo Ezabatu Egiaztaketa-batura - Misio berria Ados - Fitxategi-izena Hariak Errorea @@ -157,11 +142,9 @@ Aukeratu eskuragarri dagoen karpeta bat deskargetarako Baimen hau beharrezkoa da \nlaster-leiho moduan irekitzeko - reCAPTCHA reCAPTCHA erronka reCAPTCHA erronka eskatu da - NewPipe aplikazioari buruz Ezarpenak Honi buruz @@ -178,28 +161,22 @@ Ideiak, itzulpenak, diseinu aldaketak, kode garbiketak, kode aldaketa sakonak badituzu, laguntza beti da ongi etorria. Eginaz hobetzen da! Irakurri lizentzia Hartu parte -Harpidetu + Harpidetu Harpidetuta Kanaletik harpidetza kenduta Ezin izan da harpidetza aldatu Ezin izan da harpidetza eguneratu - Nagusia Harpidetzak - Zer dago berri - Jarraitu fokua irabaztean Jarraitu etenaldiak eta gero (adib. telefono deiak) - Deskargatu Fitxategi-izenetan baimendutako karaktereak Karaktere baliogabeak balio honekin ordezkatzen dira Ordezko karakterea - Hizkiak eta zenbakiak Karaktere berezi gehienak - Bilaketa historiala Gorde bilaketak lokalki Historiala eta katxea @@ -210,30 +187,24 @@ Historia eta cache-a Erreprodukzio-zerrenda Desegin - Atzeko planoko eta laster-leihoko NewPipe erreproduzigailuen jakinarazpenak - Emaitzarik ez Kilkerrak besterik ez daude hemen - Harpidedunik ez - Harpidedun %s - %s harpidedun - - + Harpidedun %s + %s harpidedun + Ikustaldirik ez - ikustaldi %s - %s ikustaldi - - + ikustaldi %s + %s ikustaldi + Bideorik ez - Bideoa - Bideoak - - + Bideoa + Bideoak + Historiala Bilatuta Ikusita @@ -242,32 +213,27 @@ Historiala hutsik dago Historiala garbitu da Elementua ezabatuta -Erakutsi \"mantendu eransteko\" aholkua + Erakutsi \"mantendu eransteko\" aholkua Erakutsi aholkua bigarren planoko eta laster-leihoko botoia sakatzean bideoaren xehetasunen orrian Lehenetsitako edukiaren herrialdea Zerbitzua Bigarren planoko erreproduzigailuaren ilaran Laster-leiho erreproduzigailuaren ilaran Jo denak - [Ezezaguna] - Txandakatu orientazioa Aldatu bigarren planora Aldatu laster-leihora Aldatu nagusira - Ezin izan da jario hau erreproduzitu Erreproduzigailuaren errore berreskuraezina gertatu da Erreproduzigailuaren erroretik berreskuratzen - Dohaintza NewPipe zuri esperientziarik onena ekartzeko denbora ematen duten boluntarioek garatzen dute. Emaiezu zerbait garatzaileei NewPipe kafe bat hartzen duten bitartean hobetu ahal izan dezaten. Egin dohaintza Webgunea Bisitatu NewPipe webgunea informazio gehiagorako eta berriak irakurtzeko. Elementu hau bilaketen historialetik ezabatu nahi duzu? - Orri nagusiko edukia Orri hutsa Kioskoaren orria @@ -277,7 +243,6 @@ Hautatu kanal bat Ez zara inolako kanalera harpidetu oraindik Hautatu kiosko bat - Kioskoa Joerak Lehen 50ak @@ -293,32 +258,25 @@ Hasi hemen erreproduzitzen Hasi hemen bigarren planoan Hasi hemen laster-leihoan - "Ireki tiradera " Itxi tiradera Ez da jarioen erreproduzigailurik aurkitu (VLC instalatu dezakezu). Beti Behin besterik ez - Kanpo erreproduzigailuek ez dituzte mota honetako estekak onartzen URL baliogabea Ez da bideo jariorik aurkitu Ez da audio jariorik aurkitu - Bideo erreproduzigailua Bigarren planoko erreproduzigailua Laster-leiho erreproduzigailua Galdetu beti - Informazioa eskuratzen… Kargatzen eskatutako edukia -Deskargatu jario fitxategia + Deskargatu jario fitxategia Erakutsi informazioa - Gogoko erreprodukzio-zerrendak - Gehitu hona - Erabili bilaketa azkar ez zehatza Kargatu iruditxoak Irudien cache-a ezabatuta @@ -329,7 +287,6 @@ Gehitu erlazionatutako jario bat azken jarioa jo bitartean errepikapenik gabeko ilara batean. Arazketa Fitxategia - Inportatu datu-basea Esportatu datu-basea Zure uneko historiala eta harpidetzak gainidazten ditu @@ -348,83 +305,60 @@ Fitxategi izena ezin da hutsik egon Errore bat gertatu da: %1$s Ez dago jariorik deskargatzeko eskuragarri - Arrastatu ordena aldatzeko - Sortu Ezabatu bat Ezabatu guztiak Baztertu Aldatu izena - Elementu 1 ezabatuta. - Ez dago fitxategi hau erreproduzitzeko aplikaziorik instalatuta - Elementu hau ikusitakoen historialetik ezabatu nahi duzu? Ziur elementu guztiak ezabatu nahi dituzula historialetik? Jotako azkena Ikusiena - Esportatuta Inportatuta Ez da baliozko ZIP fitxategia Ebisua: Ezin izan dira fitxategi guztiak inportatu. Honek oraingo ezarpenak gainidatziko ditu. - Laster hemen zerbait egongo da ;D - - \'Ireki\' ekintza hobetsia Lehenetsitako ekintza edukia irekitzean — %s - Galdetu beti - Erreprodukzio-zerrenda berria Ezabatu Aldatu izena Izena Gehitu erreprodukzio-zerrendara Ezarri erreprodukzio-zerrendaren iruditxo gisa - Gogoko erreprodukzio-zerrenda Kendu gogokoa - Erreprodukzio zerrenda hau ezabatu\? Erreprodukzio-zerrenda sortuta Erreprodukzio-zerrendatua Erreprodukzio zerrendaren iruditxoa aldatuta. Ezin izan da erreprodukzio-zerrenda ezabatu. - Azpititulurik ez - Doitu Bete Zoom - Automatikoki sortuak - Azpitituluak Aldatu azpitituluen testuaren eskala eta atzealdeko estiloa. Aplikazioa berrabiarazi behar da aldaketak aplikatzeko. - Gaitu LeakCanary Memoria galeren monitorizazioa. Aplikazioak agian ez du erantzungo memoriaren aitortza egin bitartean - Eman bizitza-ziklo kanpoko erroreen berri Inportatu/esportatu Inportatu Inportatu hemendik Esportatu hona - Inportatzen… Esportatzen… - Inportatu fitxategia Aurreko esportazioa - Ezin izan dira harpidetzak inportatu Ezin izan dira harpidetzak esportatu - Inporttu YouTube harpidetzak esportazio fitxategia deskargatuz: \n \n1. Joan URL honetara: %1$s @@ -437,24 +371,20 @@ \n3. Hasi saioa eskatzen zaizunean \n4. Kopiatu profilaren URL-a eraman zaizun orritik. zureID,soundcloud.com/zureid - Eragiketa honek sarearen erabilera handia egin lezake. \n \nJarraitu nahi duzu? - Erreprodukzio-abiaduraren kontrolak Tempoa Tonua Deslotu (distortsioa sor lezake) Nightcore Lehenetsia -Ezarpenak ere inportatu nahi dituzu? - + Ezarpenak ere inportatu nahi dituzu? Bilaketa ez zehatzak posizioak azkarrago baina prezisio gutxiagoz bilatzea ahalbidetzen du Desgaitu koadro txikiak ez kargatzeko, datuak eta memoria aurreztuz. Aldaketak memoria eta diskoko irudien cache-ak garbituko ditu. NewPipe Software Librea eta Copyleft da: Erabili, ikertu, partekatu eta hobetu dezakezu. Zehazki, elkarbanatzea eta aldatzea Free Software Foundation-ek argitaratutako GNU General Public License-ren 3. bertsioa edo berriagoren baten terminoen arabera egiteko baimena duzu. Behartu aktibitatearen bizitza ziklotik kanpo baztertu eta gero entregatu ezin diren Rx salbuespenen inguruko txostena - NewPipe pribatutasun politika NewPipe proiektuak aintzat hartzen du zure pribatutasuna. Aplikazioak ez du zure baimenik gabe daturik jasotzen. \nNewPipe pribatutasun politikak azaltzen du zehazki bidali eta gordetako informazioa zein den kraskatze txosten bat bidaltzen duzunean. @@ -463,20 +393,17 @@ \nAkats txosten bat bidali ahal izateko onartu behar duzu." Onartu Ukatu - Mugagabea Mugatu bereizmena datu mugikorrak erabiltzean Aurreratu azkar isilunea dagoenean Urratsa Leheneratu - Minimizatu app-a aldatzean Ekintza bideo erreproduzigailu nagusitik beste app batera aldatzean — %s Bat ere ez Minimizatu bigarren planoko erreproduzigailura Minimizatu laster-liho erreproduzigailura - -Kanalak + Kanalak Erreprodukzio-zerrendak Pistak Erabiltzaileak @@ -540,7 +467,7 @@ Garbitu amaitutako deskargak Berrekin burutzeke dauden %s transferentzia deskargetatik Gelditu - Saiakerak gehienez + Gehienezko saiakerak Deskarga ezeztatu aurretik saiatu beharreko aldi kopurua Pausatu datu mugikorretara aldatzean Pausatu ezin daitezkeen deskargak berrekingo dira From f4fa68c390ef57ac6b76833610e1f147ebd134d6 Mon Sep 17 00:00:00 2001 From: ssantos Date: Fri, 15 Mar 2019 21:36:32 +0000 Subject: [PATCH 025/138] Translated using Weblate (German) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-de/strings.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index eb891dec0..193fde6bd 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -2,8 +2,7 @@ %1$s Aufrufe Veröffentlicht am %1$s - Keinen Streamplayer gefunden. Willst du VLC installieren\? -\n\t + Keinen Stream-Player gefunden. Möchten Sie VLC installieren\? Installieren Abbrechen \n From 26991928ae2ced159338dcf62ee5befa1598e134 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Fri, 15 Mar 2019 01:28:51 +0000 Subject: [PATCH 026/138] Translated using Weblate (Chinese (Traditional)) Currently translated at 99.8% (442 of 443 strings) --- app/src/main/res/values-zh-rTW/strings.xml | 103 ++++----------------- 1 file changed, 17 insertions(+), 86 deletions(-) diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 2d46f4516..655cd0dd6 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -29,7 +29,6 @@ 主題 灰暗 明亮 - 下載 下一部 顯示「下一部」與「相關」的影片 @@ -41,7 +40,6 @@ 背景播放中 播放 網路錯誤 - 影片預覽縮圖 影片預覽縮圖 發佈者的個人頭像縮圖 @@ -52,10 +50,9 @@ 音訊下載資料夾 已下載的音訊存放存儲在這裡\t 輸入音訊檔案的下載路徑 - 無法建立下載目錄「%1$s」 已建立下載目錄「%1$s」 -輕觸搜尋按鈕開始使用 NewPipe + 輕觸搜尋按鈕開始使用 NewPipe 以懸浮視窗開啟 移除某些解析度的音訊 NewPipe 懸浮視窗模式 @@ -73,7 +70,6 @@ 使用手勢來控制播放器的亮度及音量 搜尋建議 搜尋時顯示搜尋建議 - 懸浮視窗 以懸浮視窗播放中 內容 @@ -91,7 +87,6 @@ 重新整理 清除 最佳解析度 - 調整大小 錯誤 無法載入所有縮圖 @@ -114,32 +109,24 @@ 發生了什麼:\\n請求:\\n內容語言:\\n服務:\\nGMT 時間:\\n套件:\\n版本:\\n系統版本: 您的留言(請用英文): 詳細資訊: - - 回報錯誤 使用者回報 - 影片 音訊 重試 無法存取儲存空間 使用舊式播放器 舊型內建 Mediaframework 播放器 - - 百萬 十億 - 開始 暫停 播放 刪除 檢查碼 - 新任務 - 檔案名稱 連線數 錯誤 @@ -152,25 +139,19 @@ 已複製至剪貼簿 請選擇下載資料夾 使用懸浮視窗模式需要此權限 - reCAPTCHA 驗證 reCAPTCHA 驗證 已請求 reCAPTCHA 驗證 - 懸浮視窗 - 直播 訂閱 已訂閱 已取消訂閱頻道 無法更改訂閱 無法更新訂閱 - 主頁 訂閱清單 - 新鮮事 - 搜尋紀錄 在本機儲存搜尋紀錄 歷史紀錄與快取 @@ -182,36 +163,28 @@ 歷史記錄和快取 播放清單 復原 - NewPipe 通知 NewPipe 背景播放與懸浮模式播放器的通知 - 沒有結果 這裡空空如也 - 無訂閱者 - %s 位訂閱者 - - + %s 位訂閱者 + 無觀看次數 - %s 次觀看 - - + %s 次觀看 + 沒有影片 - 影片 - - + 影片 + 下載 檔案名稱中允許的字元 不符合設定的字元將會被替換為此字串 替換為 - 字母與數字 大部分的特殊字元 - 關於 NewPipe 設定 關於 @@ -228,7 +201,6 @@ 不管你有什麼點子——翻譯、設計、程式碼整理,或者程式碼撰寫——我們永遠歡迎你來幫忙。完成的越多,NewPipe 也會更好! 閱讀授權條款 貢獻 - 歷史紀錄 已搜尋 已觀看 @@ -238,7 +210,7 @@ 已清除歷史紀錄 項目已刪除 確定要刪除此項搜尋紀錄嗎? -找不到串流播放器(您可以安裝 VLC 播放)。 + 找不到串流播放器(您可以安裝 VLC 播放)。 顯示「長按以新增」提示 預設內容國家 服務 @@ -247,14 +219,11 @@ 全部播放 總是 僅一次 - [未知] - 切換方向 切換到背景 切換到懸浮視窗 切換到主介面 - 無法播放此串流 發生無法復原的播放器錯誤 正在從播放器錯誤中復原 @@ -263,7 +232,6 @@ 無效的 URL 找不到影片串流 找不到音訊串流 - 贊助 網站 匯入資料庫 @@ -286,7 +254,6 @@ 無有效的 ZIP 檔案 警告:無法匯入所有檔案。 這將覆蓋您目前的設定。 - 互動導覽 動向 前 50 @@ -301,109 +268,82 @@ 從這裡開始播放 當背景時從這裡開始 懸浮視窗時從這裡開始 - 長按以新增至佇列 NewPipe 由志願者所開發,他們耗費時間務求為您帶來最佳體驗。現在是時候回過頭來,讓我們的開發人員能夠在使 NewPipe 更臻完美的同時,享受一杯咖啡。 打開抽屜 關閉抽屜 - 影片播放器 背景播放器 懸浮視窗播放器 總是詢問 - 正在取得資訊… 正在載入要求的內容 -下載串流檔案 + 下載串流檔案 顯示資訊 - 書籤已加入播放清單 - 新增至 - 拖曳以重新排序 - 建立 刪除 全部刪除 退出 重新命名 - 您是否要刪除此項觀看記錄? 您確定要刪除歷史記錄中的所有項目嗎? 上一次播放 最常播放 - 總是詢問 - 新的播放清單 刪除 重新命名 名稱 新增至播放清單 設為播放清單縮圖 - 將播放清單加入書籤 移除書籤 - 刪除此播放清單? 已建立播放清單 已增至播放清單 播放清單縮圖已更改。 無法刪除播放清單。 - 沒有字幕 - 合適的 填滿 縮放 - 標題字體大小 縮小字體 正常字體 加大字體 - 某些東西即將在此出現 ;D - - 監測流失 啟用記憶體流失監測技術,當 heap dump 時,應用程式可能會反應遲鈍 已停用記憶體流失監測 -除錯 + 除錯 自動產生 啟用 LeakCanary 記憶體洩漏監測,在 heap dumping 時可能導致應用程式無法回應 - 報告週期不足錯誤 強制報告在處理完片段或活動週期外發生的無法傳遞的 Rx 異常 - -使用粗略但快速的尋找 + 使用粗略但快速的尋找 粗略的尋找能讓播放器以降低的精確度更快找到影片的進度位置 自動將下一部影片新增至佇列 在非重複播放佇列中最後一個串流開始播放時,自動新增相關串流。 同步 - 檔案 - 無效的資料夾 無效的檔案/內容來源 檔案名稱不能留空 發生錯誤: %1$s - 匯入/匯出 匯入 匯入自 匯出至 - 正在匯入… 正在匯出… - 匯入檔案 無法匯入訂閱 無法匯出訂閱 - 之前的匯出 - 檔案不存在或讀取或寫入權限不足 透過下載匯出檔來匯入您的 YouTube 訂閱: \n @@ -411,11 +351,10 @@ \n2. 當被提示時登入帳號 \n3. 應該會開始下載(這就是匯出檔 ) yourID, soundcloud.com/yourid - 請記住,此操作可造成昂貴網路花費。 \n \n您是否希望繼續? -透過輸入 URL 或您的 ID 來匯入 SoundCloud 個人設定檔: + 透過輸入 URL 或您的 ID 來匯入 SoundCloud 個人設定檔: \n \n1. 在瀏覽器中啟用「桌面模式」(該網站不適用於行動裝置) \n2. 移至此網址: %1$s @@ -433,16 +372,12 @@ 解除連結(可能導致失真) Nightcore 預設 -偏好的「開啟」動作 + 偏好的「開啟」動作 開啟內容時的預設動作 — %s - 沒有可供下載的串流 - 字幕 調整播放器字幕文字大小與背景樣式。必須重新啟動應用程式才會生效。 - 未安裝可播放此檔案的應用程式 - 清除觀看歷史 刪除播放過的串流歷史 刪除所有觀看歷史記錄? @@ -452,10 +387,8 @@ 刪除所有搜尋歷史記錄? 搜尋歷史已刪除。 已刪除 1 個項目。 - NewPipe 是一個 Copyleft 的自由軟體:您可以隨意使用、研究、分享並改進它。在遵守由自由軟體基金會所發佈的 GNU 通用公共授權條款的狀況下,您可以自由地再散佈及/或修改它;授權條款預設使用第三版,但您也可以選擇更新的版本。 您是否要同時匯入設定? - NewPipe 的隱私政策 NewPipe 專案非常重視您的隱私。因此,未經您同意此程式不會收集任何資料。 \nNewPipe 的隱私權政策詳細說明了當您發送錯誤回報時,什麼資料會被傳送及儲存。 @@ -464,18 +397,16 @@ \n您必須接受它才能向我們發送錯誤報告。 接受 拒絕 -沒有限制 + 沒有限制 使用行動網路時限制解析度 切換 app 時最小化 從主影片播放器切換到其他 app 時要執行的動作 — %s 最小化為背景播放器 最小化為彈出式播放器 - -靜音時快轉 + 靜音時快轉 步進 重設 - 頻道 播放清單 @@ -538,10 +469,10 @@ 清除已結束的下載 繼續從您所擱置中的下載 %s 傳輸 停止 - 最高的重試次數 + 最大重試次數 在取消下載前的最大嘗試數 切換到行動數據時暫停 - 無法暫停下載,將重新開始 + 無法暫停的下載將會重新開始 事件 會議 \ No newline at end of file From 065820ffa45258d19bbd2ec55badfc0418bd85a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=2E=20R=C3=BCdinger?= Date: Sat, 16 Mar 2019 11:54:09 +0000 Subject: [PATCH 027/138] Translated using Weblate (German) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-de/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 193fde6bd..70759173c 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -2,7 +2,7 @@ %1$s Aufrufe Veröffentlicht am %1$s - Keinen Stream-Player gefunden. Möchten Sie VLC installieren\? + Keinen Stream-Player gefunden. Möchtest Du VLC installieren\? Installieren Abbrechen \n From 44e34d084e300a540427e787a8974a498d18f775 Mon Sep 17 00:00:00 2001 From: WaldiS Date: Thu, 14 Mar 2019 19:12:46 +0000 Subject: [PATCH 028/138] Translated using Weblate (Polish) Currently translated at 99.8% (442 of 443 strings) --- app/src/main/res/values-pl/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index cc4e717c7..d2e2d1f58 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -458,7 +458,7 @@ Nie można połączyć się z serwerem Serwer nie wysyła danych Serwer nie akceptuje pobierania wielowątkowego, spróbuj ponownie za pomocą @string/msg_threads = 1 - Żądany zakres jest niewłaściwy + Niewłaściwy zakres Nie znaleziono Przetwarzanie końcowe nie powiodło się Wyczyść ukończone pobieranie From a7156f665a6145c250d691a8f92c56a3b1a8e1f4 Mon Sep 17 00:00:00 2001 From: ssantos Date: Fri, 15 Mar 2019 21:42:17 +0000 Subject: [PATCH 029/138] Translated using Weblate (Portuguese) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-pt/strings.xml | 114 ++++--------------------- 1 file changed, 17 insertions(+), 97 deletions(-) diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 99156d49f..de0097bb3 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -32,7 +32,6 @@ URL não suportado Idioma padrão do conteúdo Vídeo e áudio - Miniatura de vídeos Miniatura de vídeos Miniatura da foto do canal @@ -41,19 +40,16 @@ Tema Escuro Claro - Aparência Outros Reprodução em segundo plano Reproduzir Erro de rede - Usar Tor (Experimental) Usar a rede Tor para aumentar a privacidade (ainda não é suportada a emissão de vídeos). Pasta para a descarga de áudio Local para guardar o áudio descarregado Digite o caminho para os ficheiros de áudio - Não foi possível criar o diretório \'%1$s\' Diretório \'%1$s\' criado com sucesso Erro @@ -62,11 +58,9 @@ Incapaz de processar o site Conteúdo não disponível Bloqueado por GEMA - Conteúdo Restringir conteúdo por idade - Mostrar vídeo com restrição de idade. É possível permitir tal material a nas Opções. - + Mostrar vídeo com restrição de idade. É possível permitir tal material nas Configurações. Não foi possível processar totalmente o site Não foi possível configurar o menu de descargas As emissões em direto ainda não são suportadas @@ -79,8 +73,6 @@ O que ocorreu: Comentários (em inglês): Detalhes: - - Vídeo Áudio Tentar novamente @@ -89,23 +81,17 @@ Reprodução automática Reproduzir vídeo se o NewPipe for invocado por outra aplicação Direto - Reportar um erro Relatório - Descargas Descargas Relatório de erro - Iniciar Pausa Reproduzir Apagar Checksum - Nova missão - - Nome do ficheiro Erro Servidor não suportado @@ -115,46 +101,34 @@ Por favor aguarde… Copiado para a área de transferência Por favor selecione a pasta para as descargas - OK Processos Descarga do NewPipe Não foi possível carregar a imagem - Aplicação encerrada O quê:\\nPedido:\\nIdioma do conteúdo:\\nServiço:\\nHora GMT:\\nPacote:\\nVersão:\\nVersão do SO: Abrir no modo \"popup\" Preto - Tudo Canais Sim Depois - - K M MM - Esta permissão é necessária \npara o modo \'popup\' - reCAPTCHA Desafio reCAPTCHA Desafio reCAPTCHA solicitado - Modo \'popup\' do NewPipe - Reproduzir em modo \"popup\" Formato de vídeo padrão Desativado - - Resolução padrão do \'popup\' Mostrar resoluções mais altas Apenas alguns dispositivos possuem suporte a vídeos 2K/4K Popup - Lembrar tamanho e posição do \'popup\' Popup Filtrar @@ -163,15 +137,12 @@ Segundo plano Remove o áudio em algumas resoluções Lembrar último tamanho e posição do \'popup\' - Redimensionar -Controlo de reprodução por gestos + Controlo de reprodução por gestos Utilizar gestos para controlar o brilho e o volume do reprodutor Sugestões de pesquisa Mostrar sugestões ao pesquisar - Melhor resolução - Acerca de NewPipe Definições Sobre @@ -188,17 +159,14 @@ Se tem ideias para: tradução, alterações de design, limpeza de código ou alterações ao código fonte - todas as ajudas são bem-vindas. Quanto mais se faz, melhor ficará! Ler licença Participar -Subscrever + Subscrever Subscrito Canal não subscrito Incapaz de alterar a subscrição Incapaz de atualizar a subscrição - Principal Subscrições - Novidades - Histórico de pesquisa Guardar termos de pesquisa localmente Histórico e cache @@ -210,39 +178,31 @@ Histórico e cache Lista de reprodução Desfazer - Notificação NewPipe Notificações para o NewPipe e para os reprodutores \"popup\" - Sem resultados Aqui não há nada para ver - Sem subscritores - %s subscritor - %s subscritores - - + %s subscritor + %s subscritores + Sem visualizações - %s visualização - %s visualizações - - + %s visualização + %s visualizações + Sem vídeos - vídeo - vídeos - - + vídeo + vídeos + Descarregar Caracteres permitidos em nomes de ficheiros Caracteres inválidos são substituídos por este valor Carácter de substituição - Letras e dígitos Caracteres especiais - Histórico Pesquisado Visualizado @@ -251,14 +211,11 @@ O histórico está vazio Histórico apagado Item apagado -Deseja apagar este item do histórico de pesquisa? -Reproduzir todos - + Deseja apagar este item do histórico de pesquisa? + Reproduzir todos [Desconhecido] - Ocorreu um erro irrecuperável do reprodutor A recuperar de um erro do reprodutor - Conteúdo da página principal Página vazia Página de subscrições @@ -266,7 +223,6 @@ Selecione um canal Não existem canais subscritos Selecione um \"kiosk\" - Kiosk Tendências Top 50 @@ -279,14 +235,11 @@ Iniciar reprodução aqui Iniciar aqui se em segundo plano Iniciar aqui em novo \'pop-up\' -Mostrar informação - + Mostrar informação Listas de reprodução favoritas - Serviço Sempre Apenas uma vez - Mudar orientação Importar base de dados Exportar base de dados @@ -297,22 +250,18 @@ Mudar para segundo plano Mudar para \'popup\' Mudar para principal - Incapaz de reproduzir este vídeo Reprodutores externos não suportam este tipo de ligações URL inválido Arraste para reordenar - Criar Apagar um Apagar todos Renomear - Doar Nenhum reprodutor encontrado (pode instalar o reprodutor VLC). Descarregar ficheiro da emissão Adicionar a - Utilizar pesquisa rápida Esta opção permite que a pesquisa seja mais rápida mas diminui a qualidade da precisão Carregar miniaturas @@ -328,7 +277,6 @@ O nome do ficheiro não pode estar vazio Ocorreu um erro: %1$s Não existem emissões para descarregar - Descartar Site Visite ao site NewPipe para obter mais informações e saber as novidades. @@ -339,7 +287,6 @@ Ficheiro ZIP inválido Aviso: nem todos os ficheiros foram importados. Está prestes a substituir a configuração atual. - Abrir menu Fechar menu Nome @@ -347,55 +294,41 @@ Remover todos os dados em cache Meta-dados em cache apagados Ficheiro - Deseja apagar este item do histórico de visualizações\? Tem a certeza de que deseja apagar todos os itens do histórico\? Última reprodução Mais reproduzido - Algo vai surgir aqui em breve ;D - - Reprodutor de vídeo Reprodutor em segundo plano Reprodutor \'popup\' Perguntar sempre - A obter informação… A carregar o conteúdo... - Nova lista de reprodução Apagar Renomear Adicionar à lista de reprodução Guardar lista de reprodução como favorita Remover marcador - Apagar esta lista de reprodução\? Lista de reprodução criada com sucesso Colocado na lista de reprodução Miniatura alterada. Sem legendas - Ampliar - Gerado automaticamente - Legendas Importar/exportar Importar Importar de Exportar para - A importar… A exportar… - Importar ficheiro Exportação anterior - Não foi possível importar as subscrições Não foi possível exportar as subscrições - Importe as subscrições do YouTube descarregando o ficheiro exportado: \n \n1. Aceda a este URL: %1$s @@ -408,10 +341,9 @@ \n3. Inicie a sessão \n4. Copie o URL do seu perfil. suaID, soundcloud.com/suaID - Controlos para velocidade de reprodução Ritmo -Limpar histórico de visualizações + Limpar histórico de visualizações Adicionar automaticamente à fila uma emissão relacionada se em filas não repetíveis. Mostrar dica \"Toque longo para colocar na fila\" Mostrar dica quando o botão de fundo ou de \'popup\' for premido na página de detalhes do vídeo @@ -427,9 +359,7 @@ Apagar todo o histórico de pesquisa\? Histórico de pesquisa apagado. 1 elemento apagado. - Não existe uma aplicação para reproduzir este ficheiro - NewPipe é desenvolvido por voluntários que usam o tempo livre para lhe proporcionar uma melhor experiência. Retribua para ajudar os programadores a tornarem o NewPipe ainda melhor. Retribuir Política de privacidade do NewPipe @@ -439,42 +369,32 @@ Colocar emissão seguinte na fila NewPipe é copyleft libre software: você pode usar, estudar, partilhar e melhorar a aplicação. Especificamente, você pode redistribuir e/ou modificar a aplicação nos termos da GNU General Public License, conforme publicada pela Free Software Foundation, tanto a versão 3 da licença ou (por sua opção) qualquer versão posterior. Deseja também importar as definições\? - Toque longo para colocar na fila Colocar em fila se estiver em segundo plano Colocar em novo \'popup\' Ação \'abrir\' preferida Ação padrão para abrir o conteúdo — %s - Definir como miniatura da lista de reprodução - Não foi possível apagar a lista de reprodução. - Ajustar Preencher Modificar escala das legendas e o estilo de fundo. Tem que reiniciar a aplicação para aplicar as alterações. - Ativar LeakCanary A monitorização de memória pode tornar a aplicação instável - Relatar erros \'out-of-lifecycle\' Forçar relatórios de exceções de Rx não entregues fora do ciclo de vida de fragmento ou atividade após a eliminação - Tenha em atenção que esta operação pode sobrecarregar a sua rede. \n \nDeseja continuar\? - Velocidade Dissociar (pode causar distorção) Avanço rápido durante silêncio Passo Repor - Para cumprir o Regulamento Geral sobre a Proteção de Dados (RGPD), chamamos a sua atenção para a política de privacidade do NewPipe. Por favor, leia com atenção. \nTem que aceitar esta política para nos poder enviar o seu relatório. Aceitar Recusar - Ilimitado Limitar resolução se estiver a usar dados móveis Minimizar ao trocar de aplicação @@ -540,7 +460,7 @@ Limpar descargas terminadas Continue suas %s transferências pendentes de descarregamentos Parar - Máximo de tentativas + Tentativas máximas Número máximo de tentativas antes de cancelar a descarga Pausa na comutação para dados móveis Descarregamentos que não podem ser pausados serão reiniciados From b017e439b152a1c7e2bf9ca64168a6e2d95e1a97 Mon Sep 17 00:00:00 2001 From: Igor Nedoboy Date: Sun, 17 Mar 2019 19:06:18 +0000 Subject: [PATCH 030/138] Translated using Weblate (Russian) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-ru/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index fa388df8b..e83b16772 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -465,7 +465,7 @@ Продолжить ожидающие загрузки (%s) Максимум попыток Максимальное число попыток перед отменой загрузки - Загрузки, которые невозможно приостановить будут перезапущены + Загрузки, которые невозможно приостановить, будут перезапущены Не удалось установить защищенное соединение Не удалось соединиться с сервером Не удалось получить данные с сервера From a7d77716f3a47c71521dd22772092d2f6884e7c7 Mon Sep 17 00:00:00 2001 From: WaldiS Date: Mon, 18 Mar 2019 07:26:34 +0000 Subject: [PATCH 031/138] Translated using Weblate (Polish) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-pl/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index d2e2d1f58..4fedc93d9 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -47,7 +47,7 @@ Odtwórz Zawartość Treści z ograniczeniem wiekowym - Pokaż wideo z ograniczeniem wiekowym. Zezwalanie na takie materiały jest możliwe w Ustawieniach. + Pokaż wideo z ograniczeniami wiekowymi. Dopuszczenie takiego materiału jest możliwe z poziomu Ustawienia. Na żywo Pobrane Pobrane From a86e8b98fe227cfd4df47f26a05449ebea4ff66f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Serdar=20Sa=C4=9Flam?= Date: Mon, 18 Mar 2019 20:36:16 +0000 Subject: [PATCH 032/138] Translated using Weblate (Turkish) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-tr/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index a2dd72807..b7c4b0156 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -47,7 +47,7 @@ Oynat İçerik Yaş kısıtlamalı içerik - Yaş kısıtlamalı videoyu göster. Bu tür materyallere Ayarlar\'dan izin verilebilir. + Yaş kısıtlamalı videoyu göster. Bu tür malzemelere Ayarlardan izin vermek mümkündür. CANLI İndirilenler İndirilenler @@ -464,13 +464,13 @@ Sunucuya bağlanılamıyor Sunucu veri göndermiyor Sunucu, çok iş parçacıklı indirmeleri kabul etmez, @string/msg_threads = 1 ile yeniden deneyin - İstenen aralık karşılanamaz + İstenen aralık karşılanamıyor Bulunamadı İşlem sonrası başarısız Tamamlanan indirmeleri temizle Beklemedeki %s transferinize İndirmeler\'den devam edin Durdur - Maksimum deneme sayısı + Azami dene sayısı İndirmeyi iptal etmeden önce maksimum deneme sayısı Mobil veriye geçerken duraklat Duraklatılamayan indirmeler yeniden başlatılacak From b141d96e7c3e195550ace5e59363eb9d33b37af4 Mon Sep 17 00:00:00 2001 From: Osoitz Date: Tue, 19 Mar 2019 20:02:45 +0000 Subject: [PATCH 033/138] Translated using Weblate (Basque) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-eu/strings.xml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index d49af0961..cd0c618ee 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -171,7 +171,7 @@ Zer dago berri Jarraitu fokua irabaztean Jarraitu etenaldiak eta gero (adib. telefono deiak) - Deskargatu + Deskargak Fitxategi-izenetan baimendutako karaktereak Karaktere baliogabeak balio honekin ordezkatzen dira Ordezko karakterea @@ -184,7 +184,7 @@ NewPipe jakinarazpena Erreproduzigailua Portaera - Historia eta cache-a + Historia eta cachea Erreprodukzio-zerrenda Desegin Atzeko planoko eta laster-leihoko NewPipe erreproduzigailuen jakinarazpenak @@ -279,10 +279,10 @@ Gehitu hona Erabili bilaketa azkar ez zehatza Kargatu iruditxoak - Irudien cache-a ezabatuta - Ezabatu cache-ko metadatuak - Kendu cache-ko wegbuneen datu guztiak - Metadatuen cache-a ezabatuta + Irudien cachea ezabatuta + Ezabatu cacheko metadatuak + Kendu cachetik webguneen datu guztiak + Metadatuen cachea ezabatuta Gehitu ilarara hurrengo jarioa Gehitu erlazionatutako jario bat azken jarioa jo bitartean errepikapenik gabeko ilara batean. Arazketa @@ -382,7 +382,7 @@ Lehenetsia Ezarpenak ere inportatu nahi dituzu? Bilaketa ez zehatzak posizioak azkarrago baina prezisio gutxiagoz bilatzea ahalbidetzen du - Desgaitu koadro txikiak ez kargatzeko, datuak eta memoria aurreztuz. Aldaketak memoria eta diskoko irudien cache-ak garbituko ditu. + Desgaitu koadro txikiak ez kargatzeko, datuak eta memoria aurreztuz. Aldaketak memoria eta diskoko irudien cacheak garbituko ditu. NewPipe Software Librea eta Copyleft da: Erabili, ikertu, partekatu eta hobetu dezakezu. Zehazki, elkarbanatzea eta aldatzea Free Software Foundation-ek argitaratutako GNU General Public License-ren 3. bertsioa edo berriagoren baten terminoen arabera egiteko baimena duzu. Behartu aktibitatearen bizitza ziklotik kanpo baztertu eta gero entregatu ezin diren Rx salbuespenen inguruko txostena NewPipe pribatutasun politika From 8073364b7a9743d3b5991af5f2a80f9e1dbc8a02 Mon Sep 17 00:00:00 2001 From: naofum Date: Mon, 18 Mar 2019 13:46:05 +0000 Subject: [PATCH 034/138] Translated using Weblate (Japanese) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-ja/strings.xml | 107 +++---------------------- 1 file changed, 11 insertions(+), 96 deletions(-) diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index c53c401a0..f704b8dbd 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -38,23 +38,18 @@ 外部プレイヤーを使用する 外部プレイヤーを使用する バックグラウンドで再生中 - 再生 - Torを使用する (実験的) 強制的に Tor を経由した経路で保存して、匿名性を高めます(動画の同時再生は未だ対応していません)。 テーマ ダーク ライト - 外観 その他 ネットワークエラー - 音楽を保存する場所 ダウンロードした音楽をここに保存します 音楽ファイルをダウンロードする場所を入力して下さい。 - 保存場所 \'%1$s\' を作成できません 保存場所 \'%1$s\' を作成しました エラー @@ -63,17 +58,11 @@ Webサイトを解析できませんでした コンテンツが利用できません GEMA にブロックされました - 保存メニューを設定できませんでした - - 生放送にはまだ対応していません - - コンテンツ 年齢制限のあるコンテンツ - この動画には年齢制限があります。視聴するには設定から制限を解除して下さい。 - + 年齢制限のビデオを表示します。そのような素材の許可は、設定から可能です。 ウェブサイトを完全には解析できませんでした 動画を取得できませんでした 申し訳ありません。発生すべきでものではありませんでした。 @@ -84,8 +73,6 @@ 何が起こりましたか: あなたの意見(英語で): 詳細: - - 動画 音楽 再試行 @@ -94,19 +81,15 @@ NewPipe が他のアプリから呼び出された時、動画を再生します。 不具合を報告 利用者報告 - 生放送 - 開始するには検索をタップ 開始 一時停止 再生 削除 チェックサム - 新しいミッション OK - ファイル名 同時接続数 エラー @@ -118,71 +101,50 @@ お待ちください… クリップボードにコピーしました ダウンロードフォルダを選択して下さい - ダウンロード ダウンロード 不具合報告 - 画像を読み込みできません - アプリ/UI がクラッシュしました 何:\\\\n提案:\\\\nコンテンツ言語:\\\\nサービス:\\\\nGMT 時間:\\\\nパッケージ:\\\\nバージョン:\\\\nOSバージョン: reCAPTCHA reCAPTCHA の要求 - reCAPTCHA を要求しました - ブラック - すべて チャンネル - - K M B - はい 後で - - ポップアップモードで開く ポップアップモードで開くには このアクセス許可が必要です - NewPipe ポップアップモード - ポップアップモードで再生中 古いプレーヤーを使用する 古い内蔵のMediaframeworkプレーヤー 無効 - デフォルトの動画形式 - デフォルトのポップアップ解像度 高い解像度で表示 2K/4K ビデオの再生は一部のデバイスのみサポートしています 背景 ポップアップ - フィルター 更新 クリア - ポップアップのサイズと位置を記憶する 前回のポップアップしたサイズと位置を記憶する - ポップアップ サイズを変更 - 一部の解像度では音声がありません プレーヤーのジェスチャー コントロール ジェスチャーを使用してプレーヤーの明るさと音量をコントロールする 検索候補 検索時に候補を表示します - -最高の解像度 - + 最高の解像度 NewPipe について 設定 このアプリについて @@ -199,17 +161,14 @@ 翻訳、デザインの変更、コードの整理、動作の重いコードの変更など、アイデアをお持ちではありませんか?ヘルプはいつでも歓迎します。より良いものを一緒に作り上げましょう! ライセンスを読む 貢献する -チャンネル登録 + チャンネル登録 チャンネル登録しました チャンネル登録を解除しました チャンネル登録を変更できません チャンネル登録を更新できません - メイン 登録リスト - 新着 - 検索履歴 検索履歴をローカルに保存します 再生履歴とキャッシュ @@ -223,10 +182,8 @@ 再生リスト 元に戻す すべて再生 - 通知 [不明] - 動画の再生ができませんでした 回復不能な再生エラーが発生しました 何も見つかりませんでした @@ -236,10 +193,8 @@ ファイル名に使用可能な文字 無効な文字は次の値で置き換えられます 置換文字 - 文字と数字 ほとんどの特殊文字 - 寄付 NewPipe は、あなたに最高の体験を味わってもらうために、ボランティアが自分たちの時間を使って開発しています。開発者たちがコーヒーを飲みながら NewPipe を継続的に改良できるよう、あなたのご支援をお願いします。 Webサイト @@ -253,7 +208,6 @@ 履歴を消去しました アイテムを削除しました このアイテムを検索履歴から削除しますか? - メインページのコンテンツ 空白ページ キオスクページ @@ -263,7 +217,6 @@ 選択したチャンネル 購読しているチャンネルはありません 選択したキオスク - キオスク 人気 トップ50 @@ -275,13 +228,11 @@ バックグラウンド再生に変更 ポップアップ再生に変更 メイン再生に変更 - 動画プレイヤーが見つかりません (VLCをインストールして再生できます)。 デフォルトのコンテンツの国 サービス 常に 一度だけ - データベースのインポート データベースのエクスポート 既存の履歴と購読リストは上書きされます @@ -294,25 +245,20 @@ 有効な ZIP ファイルではありません 警告: すべてのファイルをインポートできませんでした。 これにより、現在の設定が上書きされます。 - バックグラウンド再生 ここから再生を開始 ここからバックグランド再生を開始 ドロワーを開く ドロワーを閉じる - 動画プレーヤー バックグラウンドプレーヤー ポップアッププレーヤー 常に尋ねる - 情報を取得しています… コンテンツを読み込んでいます -動画ファイルをダウンロード + 動画ファイルをダウンロード 情報を表示 - ブックマーク - サムネイルを読み込む 画像キャッシュを消去しました メタデータのキャッシュを消去 @@ -321,7 +267,6 @@ 次の動画を自動でキューに追加 デバッグ ファイル - 動画が見つかりません 音声が見つかりません そのフォルダーはありません @@ -329,56 +274,44 @@ ファイル名は空白にできません エラーが発生しました: %1$s ダウンロードできる動画はありません - ドラッグして並べ替え - 作成 削除 すべて削除 破棄 名前を変更 - このアイテムを再生履歴から削除しますか? すべてのアイテムを再生履歴から削除しますか? 常に確認 - 新規プレイリスト 削除 変更 プレイリスト名 プレイリストに追加 プレイリストのサムネイルとして設定 - プレイリストをブックマーク ブックマークを削除 - このプレイリストを削除しますか? プレイリストが作成されました プレイリストに追加しました プレイリストのサムネイルを変更しました。 プレイリストが削除できませんでした。 - 字幕なし - 字幕の文字サイズ インポート/エクスポート インポート インポート元 エクスポート先 - インポートしています… エクスポートしています… - ファイルからインポート 前回のエクスポート - 購読リストがインポートできませんでした 購読リストがエクスポートできませんでした - テンポ 音程 デフォルト -バックグラウンド再生の順番待ちに追加 + バックグラウンド再生の順番待ちに追加 ポップアップ再生の順番待ちに追加 再生履歴を消去 再生した動画の履歴を削除します @@ -387,9 +320,7 @@ 検索キーワードの履歴を削除します 検索履歴を削除しました。 このファイルを再生するためのアプリがインストールされていません - 設定をインポートしますか? - 字幕 チャンネル プレイリスト @@ -404,52 +335,38 @@ すべての検索履歴を削除します? このファイル/コンテンツはありません - 登録者数 %s 人 - - + 登録者数 %s 人 + 視聴なし - 視聴回数 %s 回 - - + 視聴回数 %s 回 + - 本の動画 - - + 本の動画 + 1 つのアイテムが削除されました - 支援する NewPipe プロジェクトはあなたのプライバシーを非常に大切にしています。あなたの同意がない限り、アプリはいかなるデータも収集しません。NewPipe のプライバシー・ポリシーでは、クラッシュリポート送信時にどのような種類のデータが送信・記録されるかを詳細に説明しています。 NewPipe は著作権が自由のソフトウェアです。あなたは自由にそれを使用し、研究し、そして改善することができます。あなたは、GNU フリーソフトウェア財団が公開する GNU General Public ライセンス バージョン3以降の下に、自由に再配布・修正を行うことができます。 最終再生日時 最も再生した動画 - ズーム - 追加... - 「長押しして追加」のヒントを表示 トラック NewPipe バックグラウンドおよびポップアップのプレーヤーの通知 - 新しい & ホット 長押ししてキューに入れる バックグラウンド時にキューに入れる ポップアップ時にキューに入れる 新ポップアップ時にここから開始 - すぐにここに表示されます;D - - お好みの \'開く\' アクション コンテンツを開くときのデフォルト動作 — %s - フィット 埋める 自動生成 - プレーヤーのキャプション文字スケールと背景スタイルを変更します。有効にするにはアプリの再起動が必要です。 - 何もありません 保存したエクスポートファイルからYoutubeの購読をインポート: \n @@ -457,10 +374,8 @@ \n2. ログインしていなければログインします \n3. ダウンロードが始まります (これがエクスポートファイルです) リセット - 同意する 拒否 - 制限なし モバイルデータ使用時の解像度の制限 アプリ切り替え時の最小化 From 73a5b6738d455a0f37103f5181b20a3aa935fd19 Mon Sep 17 00:00:00 2001 From: Andrea Gelmini Date: Wed, 20 Mar 2019 16:53:19 +0000 Subject: [PATCH 035/138] Translated using Weblate (Italian) Currently translated at 99.1% (439 of 443 strings) --- app/src/main/res/values-it/strings.xml | 141 +++++-------------------- 1 file changed, 25 insertions(+), 116 deletions(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index b8cebdacb..e75d4f89c 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -30,7 +30,6 @@ URL non supportato Lingua predefinita per i contenuti Video e Audio - Miniatura anteprima video Miniatura anteprima video Miniatura dell\'immagine di profilo dell\'utente @@ -40,20 +39,16 @@ Creata la cartella per i download \'%1$s\' Usa un lettore video esterno Usa un lettore audio esterno - Cartella degli audio scaricati Cartella in cui salvare gli audio scaricati Inserisci la cartella per gli audio scaricati - Tema Scuro Chiaro - Aspetto Altro Riproduzione in sottofondo Riproduci - Errore Errore di connessione Impossibile caricare tutte le miniature @@ -62,23 +57,16 @@ Bloccato da GEMA Usa Tor (Sperimentale) Forza il download tramite Tor per una maggiore riservatezza (lo streaming dei video non è ancora supportato). - Impossibile analizzare il sito web Impossibile impostare il menu di download - - I contenuti in diretta non sono al momento supportati - - Contenuti Contenuti vietati ai minori Mostra video riversati a un pubblico maggiorenne. Si possono abilitare dalle Impostazioni. - Tocca Cerca per iniziare Riproduzione automatica Riproduci i video quando NewPipe viene aperto da un\'altra app IN DIRETTA - Impossibile analizzare completamente il sito web Impossibile ottenere alcun flusso Spiacenti, non sarebbe dovuto succedere. @@ -89,11 +77,8 @@ Cosa è successo: Il tuo commento (in inglese): Dettagli: - - Segnala un errore Segnalazione dell\'utente - Video Audio Riprova @@ -101,16 +86,13 @@ Download Download Segnalazione errori - Inizia Pausa Riproduci Elimina Checksum - Nuovo obiettivo OK - Nome del file Thread Errore @@ -122,41 +104,28 @@ Attendi… Copiato negli appunti Seleziona una cartella per il salvataggio dei download - Impossibile caricare l\'immagine L\'app/UI si è interrotta Cosa:\\nRichiesta:\\nLingua contenuto:\\nServizio:\\nOrario GMT:\\nPacchetto:\\nVersione:\\nVersione SO: - reCAPTCHA Risoluzione reCAPTCHA - Nero - Tutto Canale - - K M B - È richiesta la risoluzione del reCAPTCHA - Più tardi - - Apri in modalità popup Modalità popup di NewPipe - Riproduzione in modalità popup Disattivato - Usa il vecchio lettore multimediale -Non riproduce l\'audio con ALCUNE risoluzioni + Non riproduce l\'audio con ALCUNE risoluzioni In sottofondo Popup - Risoluzione predefinita per la modalità popup Mostra risoluzioni più alte Solo alcuni dispositivi supportano la riproduzione video in 2K e 4K @@ -167,19 +136,15 @@ Usa i gesti per controllare la luminosità e il volume del lettore multimediale Suggerimenti di ricerca Mostra i suggerimenti durante la ricerca - Popup Filtra i risultati Ricarica Cancella Ridimensionamento Risoluzione migliore - Precedente lettore multimediale Mediaframework integrato - Questo permesso è necessario \nper riprodurre in modalità popup - Impostazioni Informazioni Licenze di terze parti @@ -195,33 +160,26 @@ Un aiuto è sempre il benvenuto: nuove idee e funzionalità, traduzioni, modifiche al design o cambiamenti radicali del codice rendono l\'applicazione sempre migliore! Leggi la licenza Contribuisci -Informazioni su NewPipe + Informazioni su NewPipe Iscriviti Iscritto Disiscritto dal canale Impossibile cambiare l\'iscrizione Impossibile aggiornare l\'iscrizione - Iscrizioni - Novità - Cronologia ricerche Salva le ricerche Cronologia & Cache Salva la cronologia dei video visualizzati Riprendi tornando in primo piano Continua a riprodurre dopo le interruzioni (es. chiamate) - - Scarica Caratteri ammessi nei nomi dei file I caratteri non validi vengono sostituiti con Carattere sostitutivo - Lettere e cifre Caratteri speciali - Cronologia Ricerche effettuate Visualizzati @@ -229,41 +187,34 @@ Cronologia La cronologia è vuota Cronologia cancellata - -Principale + Principale Lettore multimediale Comportamento Cronologia e cache Playlist Annulla - Notifiche NewPipe Notifiche per NewPipe in background e per il lettore a comparsa - Nessun risultato Nessun iscritto - %s iscritto - %s iscritti - - + %s iscritto + %s iscritti + Nessuna visualizzazione - %s visualizzazione - %s visualizzazioni - - + %s visualizzazione + %s visualizzazioni + Nessun video - Video - Video - - + Video + Video + Elemento eliminato -Nulla da mostrare - + Nulla da mostrare Vuoi eliminare questo elemento dalla cronologia? -Contenuto della pagina principale + Contenuto della pagina principale Pagina vuota Locandina Pagina iscrizione @@ -272,35 +223,31 @@ Seleziona un canale Nessuna iscrizione ad un canale Seleziona una locandina - Locandina Tendenze Top 50 New & hot -Mostra il suggerimento \"Tenere premuto per aggiungere video alla coda\" + Mostra il suggerimento \"Tenere premuto per aggiungere video alla coda\" Mostra suggerimento quando il pulsante per la riproduzione popup o in sottofondo viene premuto nella pagina dei dettagli del video In coda al lettore multimediale in sottofondo In coda al lettore multimediale a comparsa Riproduci tutto - Impossibile riprodurre questo flusso Si è verificato un errore irreversibile Ripristino dell\'errore del lettore multimediale - Riproduzione in sottofondo Riproduzione in modalità a comparsa Rimuovi Dettagli Impostazioni audio Tenere premuto per aggiungere alla coda -[Sconosciuto] - + [Sconosciuto] In coda in sottofondo In coda nel riproduttore a comparsa Inizia la riproduzione qui Inizia qui in sottofondo Inizia qui nel riproduttore a comparsa -Dona + Dona Sito web Visita il sito web di NewPipe per maggiori informazioni e novità. NewPipe è sviluppato da volontari che trascorrono il loro tempo libero per offrire la migliore esperienza. Restituisci il favore per aiutare gli sviluppatori a rendere NewPipe ancora più piacevole mentre si gustano una tazza di caffè. @@ -310,27 +257,23 @@ Passa alla riproduzione in background Passa alla visualizzazione popup Passa alla produzione predefinita - Servizio Apri il menu Chiudi il menu Nessun lettore multimediale trovato. Puoi installare VLC per riprodurlo. Sempre Solo una volta - I lettori multimediali esterni non supportano questa tipologia di collegamenti URL non valido Nessun flusso video trovato Nessun flusso audio trovato - Lettore video Riproduzione in sottofondo Riproduzione in modalità popup Chiedi sempre - Raccogliendo informazioni… Caricamento del contenuto richiesto -Importa database + Importa database Esporta database Sovrascrive la cronologia corrente e le iscrizioni Esporta la cronologia, le iscrizioni e le playlist @@ -339,94 +282,70 @@ Nessun file ZIP valido Attenzione: Impossibile importare tutti i file. Questa operazione sostituirà le tue impostazioni attuali. - Scarica il video Mostra informazioni - Playlist preferite - Aggiungi a - Trascina per riordinare - Crea Elimina uno Elimina tutti Ignora Rinomina - Vuoi eliminare questo elemento dalla cronologia delle visualizzazioni? Sei sicuro di voler eliminare tutti gli elementi dalla cronologia? Ultima riproduzione I più riprodotti - Chiedi sempre - Nuova scaletta Elimina scaletta Rinomina scaletta Nome Aggiunti alla playlist Imposta come miniatura della playlist - Segnalibri playlist Rimuovi segnalibro - Eliminare questa scaletta\? Playlist creata Aggiunti alla scaletta Miniatura della scaletta cambiata. Impossibile eliminare la scaletta. - No sottotitoli - Rientrato Pieno Ingrandito - Dimensione dei caratteri dei sottotitoli Carattere più piccolo Carattere normale Carattere più grande - - A breve qualcosa apparirà qui ;D - -Debug + A breve qualcosa apparirà qui ;D + Debug Generato automaticamente Abilita LeakCanary Il monitoraggio delle perdite di memoria potrebbe causare la mancata risposta dell\'applicazione durante il dumping dell\'heap - Segnala Errori \"Out-of-lifecycle\" Forza la segnalazione di eccezioni Rx non consegnabili al di fuori del ciclo di vita dell\'attività dopo la chiusura - -Usa la ricerca rapida ma imprecisa + Usa la ricerca rapida ma imprecisa La ricerca imprecisa permette al lettore multimediale di spostarsi nelle posizioni più velocemente con una precisione ridotta Metti in coda automaticamente il prossimo flusso Aggiungi automaticamente un flusso correlato quando è in corso la riproduzione dell\'ultimo flusso in una coda non ripetitiva. SINCRONIZZAZIONE - File - Nessuna cartella Nessun file o cartella che contiene sorgenti Il file non esiste o non si hanno i permessi sufficienti di scrittura o lettura Il nome del file non può essere vuoto Si è verificato un errore: %1$s - Importa/esporta Importa Importa da Esporta in - Importando… Esportando… - Importa file Esportazione precedente - Impossibile importare le iscrizioni Impossibile esportare le iscrizioni - Importa le iscrizioni di YouTube scaricando il file d\'esportazione: \n \n1. Vai a questo URL: %1$s @@ -439,11 +358,10 @@ \n3. Accedi quando richiesto \n4. Copia l\'URL del profilo a cui vieni indirizzato. iltuoID, soundcloud.com/iltuoid - Tieni presente che questa operazione può consumare una grande quantità di traffico dati. \n \nVuoi continuare? -Carica miniature + Carica miniature Disabilita il caricamento delle miniature, risparmiando dati e memoria. La modifica dell\'opzione comporta la cancellazione della cache in memoria e sul disco. Pulizia della cache delle immagini completata Pulisci la cache dei metadati @@ -455,16 +373,12 @@ Scollega (può causare distorsione) Nightcore Valore predefinito -Nessun flusso disponibile per il download - + Nessun flusso disponibile per il download \'Apri\' preferibilmente con Azione predefinita all\'apertura del contenuto — %s - Sottotitoli Modifica la dimensione e gli stili di sfondo dei sottotitoli. Per applicare le modifiche è richesto un riavvio. - Nessuna app installata per riprodurre questo file - Pulisci cronologia visualizzazioni Elimina la cronologia dei flussi riprodotti Elimina l\'intera cronologia delle visualizzazioni\? @@ -474,10 +388,8 @@ Elimina l\'intera cronologia delle ricerche\? Cronologia delle ricerche eliminata. 1 elemento eliminato. - NewPipe è un software libero con licenza copyleft: si può utilizzarlo, studiarlo, condividerlo e migliorarlo a proprio piacimento. In particolare, è possibile ridistribuirlo e/o modificarlo secondo i termini della GNU General Public License pubblicata dalla Free Software Foundation, sia nella versione 3 della Licenza, sia (a propria discrezione) in qualsiasi versione successiva. Vuoi anche importare le impostazioni? - Informativa sulla privacy Il progetto NewPipe tiene molto alla tua privacy. Perciò, l\'app non raccoglie alcun dato senza il tuo consenso. \nL\'informativa sulla privacy di NewPipe spiega in dettaglio quali dati vengono inviati e memorizzati quando si invia un rapporto sugli arresti anomali. @@ -486,20 +398,17 @@ \nDevi accettarla per inviarci il bug report. Accetto Rifiuto - Senza limiti Limita la risoluzione quando si utilizzano dati mobili Avanzamento veloce durante il silenzio Step Reset - Minimizza al cambio dell\'applicazione Azione quando si passa ad un\'altra app dal lettore video principale — %s Nessuna Minimizza al lettore in sottofondo Minimizza al lettore popup - -Canali + Canali Playlist Tracce Utenti @@ -564,7 +473,7 @@ Tentativi massimi Tentativi massimi prima di cancellare il download Metti in pausa quando si usano i dati mobili - I download che non possono essere messi in pausa verranno ravviati + I download che non possono essere messi in pausa verranno riavviati Eventi Conferenze \ No newline at end of file From 6a5f2402c767774c90e25315a7022027afd8f924 Mon Sep 17 00:00:00 2001 From: Le Poisson Libre Date: Fri, 22 Mar 2019 18:45:30 +0000 Subject: [PATCH 036/138] Translated using Weblate (French) Currently translated at 97.3% (431 of 443 strings) --- app/src/main/res/values-fr/strings.xml | 123 +++++-------------------- 1 file changed, 21 insertions(+), 102 deletions(-) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 98ed64f37..7838e841c 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -30,7 +30,6 @@ URL non supportée Vidéo & audio Autre - Miniature d’aperçu vidéo Miniature d’aperçu vidéo Je n’aime pas @@ -41,32 +40,26 @@ Utiliser un lecteur audio externe Lecture en arrière-plan Lire - Utiliser Tor (Expérimental) Rediriger le trafic de téléchargement via Tor pour plus de confidentialité (streaming non supporté pour le moment). Thème Sombre Clair - Apparence Erreur réseau - Dossier de téléchargement audio L\'audio téléchargé est stocké ici Entrez le chemin de téléchargement des fichiers audio - Impossible de créer le répertoire de téléchargement « %1$s » Répertoire de téléchargement « %1$s » créé Erreur Impossible d\'analyser le site web Contenu non disponible Bloqué par GEMA - Désolé, des erreurs se sont produites. Contenu Contenu avec limite d\'âge EN DIRECT - Impossible de charger toutes les miniatures Impossible de déchiffrer la signature URL de la vidéo Impossible d\'analyser complètement le site web @@ -78,8 +71,6 @@ Ce qui s\'est passé : Votre commentaire (en anglais) : Détails : - - Signaler une erreur Vidéo Audio @@ -89,23 +80,19 @@ Lecture automatique Afficher les vidéos soumises à une limite d\'âge. Autoriser ce type de contenu est possible depuis les paramètres. Rapport utilisateur - SIGNALER Impossible de configurer le menu de téléchargement Impossible d\'obtenir un flux Téléchargements Téléchargements Rapport d\'erreur - Lire Pause Lire Supprimer Somme de contrôle - Nouveau OK - Nom du fichier Threads Erreur @@ -117,71 +104,53 @@ Veuillez patienter… Copié dans le presse-papiers Sélectionner un dossier de téléchargement disponible - Impossible de charger l\'image L’application a crashé - reCAPTCHA Noir - Tout Chaîne - - Défi reCAPTCHA Défi reCAPTCHA demandé - Ouvrir en mode fenêtré Mode fenêtré NewPipe - Lecture en mode fenêtré Oui Plus tard Désactivé - Quoi :\\nRequête :\\nLangue du contenu :\\nService :\\nHeure GMT :\\nPaquet :\\nVersion :\\nVersion du système : Utiliser l\'ancien lecteur Ancienne version du lecteur Mediaframework K M - Cette autorisation est nécessaire pour \nutiliser le mode fenêtré - Arrière-plan Fenêtre - Résolution de la fenêtre par défaut Afficher résolutions plus élevées Seulement certains appareils supportent la lecture 2K/4K Format vidéo par défaut Mémoriser la taille et la position de la fenêtre Mémoriser la dernière taille et position de la fenêtre - Fenêtre Filtre Actualiser Effacer Redimensionner - B - Supprime l\'audio à CERTAINES résolutions Utiliser les gestes pour contrôler la luminosité et le volume du lecteur Suggestions de recherche Afficher les suggestions lors d\'une recherche - -Gestes pour contrôler la lecture + Gestes pour contrôler la lecture Meilleure résolution - S\'abonner Abonné Désabonné de la chaîne Principal Abonnements - Nouveautés - Téléchargement Paramètres À propos @@ -196,19 +165,16 @@ Licence de NewPipe Lire la licence Contribuer -Lettres et chiffres + Lettres et chiffres À propos de NewPipe © %1$s par %2$s sous %3$s Que ce soit pour des idées, traductions, changements de design, nettoyage ou gros changements de code, l\'aide est toujours la bienvenue. Plus on contribue, meilleur il devient ! Impossible de modifier l\'abonnement Impossible d\'actualiser l\'abonnement - Continuer la lecture après les interruptions (ex : appels) - Caractères autorisés dans les noms de fichiers Les caractères invalides sont remplacés par cette valeur Caractère de remplacement - Historique de recherche Conserver les recherches sur l\'appareil Historique et cache @@ -219,10 +185,8 @@ Historique L\'historique est vide Historique supprimé - -Notification NewPipe + Notification NewPipe Annuler - Garder une trace des vidéos regardées Reprendre sur le gain de focus Lecteur @@ -230,33 +194,27 @@ Historique & cache Liste de lecture Notifications pour les lecteurs \"arrière-plan\" et \"fenêtre\" de NewPipe - Aucun résultat Aucun contenu - Aucun abonné - %s abonné - %s abonnés - - + %s abonné + %s abonnés + Aucune vue - %s vue - %s vues - - + %s vue + %s vues + Aucune vidéo - Vidéo - Vidéos - - + Vidéo + Vidéos + Caractères spéciaux - Élément effacé -Voulez-vous supprimer cet élément de l\'historique de recherche ? -Contenu de la page principale + Voulez-vous supprimer cet élément de l\'historique de recherche ? + Contenu de la page principale Page vide Abonnements Page de Flux @@ -265,10 +223,9 @@ Populaires Top 50 Nouveau & populaire -En file d\'attente sur le lecteur en arrière-plan + En file d\'attente sur le lecteur en arrière-plan En file d\'attente sur le lecteur en fenêtré Tout lire - Impossible de jouer ce flux Une erreur irrécupérable du lecteur s\'est produite Pas encore d\'abonnements de chaînes @@ -280,12 +237,9 @@ Afficher l\'astuce « Maintenir pour ajouter » Afficher l\'aide \"Appui long pour mettre en file d\'attente\" en appuyant sur les boutons \"Arrière-plan\" et \"Fenêtre\" sur la page de détails d\'une vidéo [Inconnu] - Récupération de l\'erreur du lecteur - Page Kiosque Sélectionner un kiosque - Kiosque Appui long pour mettre en file d\'attente Mettre en file d\'attente en arrière-plan @@ -293,7 +247,7 @@ Commencer la lecture ici Démarrer ici en arrière-plan Démarrer ici en fenêtré -Donner + Donner NewPipe est développé par des volontaires sur leur temps libre afin de vous proposer la meilleure expérience possible. Vous pouvez leur offrir un café pour les soutenir dans leurs efforts et rendre NewPipe encore meilleur. Site Visitez le site internet de NewPipe pour plus d\'informations et de nouvelles. @@ -303,24 +257,20 @@ Arrière-plan Fenêtré Normal - Service Ouvrir le menu Fermer le menu Aucun lecteur de flux trouvé (vous pouvez installer VLC pour le lire). Toujours Une seule fois - Les lecteurs externes ne supportent pas ces types de liens Lien non valide Aucun flux vidéo trouvé Aucun flux audio trouvé - Lecteur vidéo Lecteur en arrière-plan Lecteur en fenêtré Toujours demander - Obtention des infos… Chargement du contenu Importer les données @@ -332,53 +282,39 @@ Aucun fichier ZIP valide Avertissement: Impossible d\'importer tous les fichiers. Cela effacera vos paramètres actuels - Afficher les infos - Marque-pages - Ajouter à - Faites glisser pour réorganiser - Créer Ignorer Renommer - Dernière lecture Tout supprimer Voulez-vous supprimer cet élément de votre historique \? Êtes-vous sûr de supprimer tout votre historique \? Vidéos les plus regardées - Toujours demander - Nouvelle liste de lecture Supprimer Renommer Nom Ajouter à la playlist Enregistrer la playlist en local - Marquer cette playlist Retirer la marque - Supprimer cette liste de lecture \? Liste de lecture créée Ajouté à la liste de lecture Miniature de la liste de lecture changée. Impossible de supprimer la liste de lecture. - Aucuns sous-titres - Ajuster Zoom - Taille des sous-titres Petite Normale Grande - Recherche rapide approximative Permettre au lecteur d\'accéder plus rapidement à une position au détriment de la précision Charger les miniatures @@ -388,44 +324,33 @@ Effacer toutes les pages web mises en cache Données en cache effacées Fichier - Aucun dossier de ce type Aucun fichier/contenu de ce type Le fichier n’existe pas ou n’est pas accessible en lecture/écriture Le nom du fichier ne peut être vide Une erreur s\'est produite: %1$s - Supprimer un seul média Quelque chose va bien bientôt arriver ;D - - Télécharger le fichier de flux Vidéo suivante en file d\'attente Ajout automatique d\'un morceau suggéré lors de la lecture du dernier morceau dans une file d\'attente non bouclée. - Débogage Remplir Générés automatiquement Activer LeakCanary La surveillance de la mémoire peut mettre temporairement l\'application en pause pendant les nettoyages - Signaler les erreurs de développement hors cycle Forcer le signalement des exceptions Rx qui surviennent hors activité - Importer/exporter Importer Importer de Exporter vers - Importation en cours… Exporation en cours… - Importer fichier Export précédent - Impossible d\'importer des abonnements Impossible d\'exporter les abonnements - "Pour importer vos abonnements YouTube vous devez d\'abord télécharger un fichier spécial de YouTube, selon les modalités suivantes : \n \n1. Suivez ce lien : %1$s @@ -438,27 +363,21 @@ \n3. Connectez-vous à votre compte. \n4. Copier l’URL vers lequel vous venez d’être redirigé. votreID, soundcloud.com/votreid - Cette opération peut consommer beaucoup de données mobiles. \n \nSouhaitez-vous continuer ? - Vitesse de lecture Cadence Détacher (déformations possibles) Défaut -Ouvrir de préférence avec + Ouvrir de préférence avec Action par défaut lors de l\'ouverture de contenu - %s - Aucun flux disponible au téléchargement - Sous-titres Modifier la taille du texte et les styles d\'arrière-plan du lecteur. Redémarrage requis pour prendre effet. - Ton Nightcore Aucune application installée pour lire ce fichier - Effacer l\'historique Supprimer l\'historique des flux regardés Supprimer tout l\'historique regardé \? @@ -468,15 +387,13 @@ Supprimer tout l\'historique de recherche \? Historique des recherches effacé. 1 élément supprimé. - "NewPipe est un logiciel sous licence libre : Vous pouvez l\'utiliser, l\'étudier, le partager et l\'améliorer comme bon vous semble. Vous pouvez le redistribuer et/ou le modifier sous les termes de la licence générale publique GNU, comme publiée par la Free Software Foundation, dans sa version 3, ou, à votre convenance, dans une version plus récente." Politique de confidentialité de NewPipe Lire la politique de confidentialité Voulez-vous également importer des paramètres \? - Accepter Refuser -Le projet NewPipe prend votre vie privée très à cœur. Ainsi, l’application n’envoie aucune donnée sans votre consentement. + Le projet NewPipe prend votre vie privée très à cœur. Ainsi, l’application n’envoie aucune donnée sans votre consentement. \nLa politique de confidentialité de NewPipe explique en détail quelles données sont envoyées et stockées lorsque vous envoyez un rapport de plantage. Afin de se conformer au Règlement Général sur la Protection des Données (RGPD ou GDPR), nous attirons votre attention sur la politique de vie privée de NewPipe. Merci de la lire attentivement. \nVous devez l\'accepter pour nous envoyer le rapport de bug. @@ -489,7 +406,6 @@ Accélérer pendant les silences Étape Réinitialiser - Minimiser lors du changement d\'application Action lors du changement d\'application depuis le lecteur vidéo —%s Aucune @@ -527,4 +443,7 @@ En file En pause Téléchargement échoué + Conférences + Téléchargement terminé + %s téléchargements terminés \ No newline at end of file From bef84e9eec5a43bc4e1f92ab07f149cb21fd7694 Mon Sep 17 00:00:00 2001 From: Florian Date: Fri, 22 Mar 2019 18:49:05 +0000 Subject: [PATCH 037/138] Translated using Weblate (French) Currently translated at 97.3% (431 of 443 strings) --- app/src/main/res/values-fr/strings.xml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 7838e841c..c6aa99bfb 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -440,10 +440,28 @@ Une mise à jour de NewPipe disponible ! Appuyez pour télécharger Terminé - En file + Dans la file d\'attente En pause Téléchargement échoué Conférences Téléchargement terminé %s téléchargements terminés + Ajouté à la file d\'attente + Générer un nom unique + Écraser + Un fichier téléchargé avec ce nom existe déjà + Il y a un téléchargement en cours avec ce nom + Afficher l\'erreur + Code + Le fichier ne peut pas être créé + Le dossier de destination ne peut pas être créé + Autorisation refusée par le système + Échoué de la connexion sécurisée + Le serveur est introuvable + Impossible de se connecter au serveur + Le serveur n’envoie pas de données + Introuvable + Effacer les téléchargements terminés + Mettre en pause lors du passage en données mobiles + Les téléchargements qui ne peuvent pas être mis en pause seront redémarrés \ No newline at end of file From e92a5414d117c5b2906ffef7123310f69e9958de Mon Sep 17 00:00:00 2001 From: Ahanaf Taskin Ar-Rafee Date: Sun, 24 Mar 2019 04:27:36 +0000 Subject: [PATCH 038/138] Translated using Weblate (Bengali (Bangladesh)) Currently translated at 30.5% (135 of 443 strings) --- app/src/main/res/values-bn-rBD/strings.xml | 64 +++++++--------------- 1 file changed, 20 insertions(+), 44 deletions(-) diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml index 3fb51af36..d6f061888 100644 --- a/app/src/main/res/values-bn-rBD/strings.xml +++ b/app/src/main/res/values-bn-rBD/strings.xml @@ -1,4 +1,4 @@ - + শুরু করতে অনুসন্ধান এ আলতো চাপ %1$s বার দেখা হয়েছে @@ -8,30 +8,26 @@ "বাদ দিন " "ব্রাউজারে ওপেন করো " - "popup মোডে Open করো " + "পপ-আপ মোডে ওপেন করো " শেয়ার - ডাউনলোড - খোঁজ - "সেটিংস " + ডাউনলোউড + খুঁজুন + সেটিংস আপনি কি বুঝিয়েছেনঃ %1$s ? - শেয়ার কর - ব্রাউজার পছন্দ কর + শেয়ার করুন + ব্রাউজার বাছাই করুন রোটেশন - বাহ্যিক ভিডিও প্লেয়ার ব্যবহার করো - বাহ্যিক অডিও প্লেয়ার ব্যবহার করো + বাইরের ভিডিও প্লেয়ার ব্যবহার করুন + বহির্গত অডিও প্লেয়ার ব্যবহার করুন NewPipe পপআপ মোড - ব্যাকগ্রাউন্ড পপআপ - ভিডিও ডাউনলোড করার পাথ ডাউনলোড করা ভিডিওগুলো রাখার ফোল্ডার ভিডিওগুলির জন্য ডাউনলোডের পাথ প্রবেশ করাও - অডিও ডাউনলোড পাথ ডাউনলোড করা অডিও সঞ্চয় করার পাথ অডিও ফাইলগুলির জন্য ডাউনলোডের পাথ প্রবেশ করাও - স্বয়ংক্রিয়ভাবে প্লে করো যখন অন্য অ্যাপ্লিকেশন থেকে চালু করা হয় স্বয়ংক্রিয়ভাবে একটি ভিডিও প্লে করো যখন NewPipe অন্য অ্যাপ্লিকেশন থেকে চালু করা হয়। ডিফল্ট রেজোল্যুশন @@ -52,7 +48,6 @@ কালো পপআপ আকার এবং অবস্থান মনে রাখো শেষ আকার এবং পপআপ সেট অবস্থান মনে রাখো - ডাউনলোড পরবর্তী ভিডিও পরবর্তী এবং অনুরূপ ভিডিওগুলি দেখাও @@ -83,7 +78,6 @@ রিফ্রেশ পরিষ্কার আকার পরিবর্তন - ত্রুটি নেটওয়ার্ক ত্রুটি @@ -108,8 +102,6 @@ কি হয়েছিল: তোমার মন্তব্য (ইংরেজিতে): বর্ণনা: - - ভিডিও প্রাকদর্শন থাম্বনেইল ভিডিও প্রাকদর্শন থাম্বনেইল @@ -120,34 +112,26 @@ (পরীক্ষামূলক) গোপনীয়তা বর্ধিত করতে টর এর মাধ্যমে ডাউনলোড ট্রাফিক জোরপুর্বক পাঠাও (ভিডিওগুলি স্ট্রিমিং এ সমর্থিত নয়)। একটি ত্রুটি রিপোর্ট করো ব্যবহারকারীর প্রতিবেদন - \'%1$s\' ডাউনলোড ডিরেক্টরি তৈরি করতে পারছে না \'%1$s\' ডাউনলোড ডিরেক্টরি তৈরি করা হয়েছে - ভিডিও অডিও পুনরায় চেষ্টা করো স্টোরেজ অ্যাক্সেস করার অনুমতি অস্বীকার করা হয়েছে পুরানো প্লেয়ার ব্যবহার করো মিডিয়াফ্রেমওয়ার্ক প্লেয়ারের পুরানো বিল্ড। - - K M B - শুরু বিরতি প্রদর্শন ডিলেট চেকসাম - নতুন মিশন ঠিক আছে - - ফাইলের নাম থ্রেড @@ -161,24 +145,18 @@ ক্লিপবোর্ডে অনুলিপি করা হয়েছে। অনুগ্রহ করে একটি উপলব্ধ ডাউনলোড ডিরেক্টরি নির্বাচন করো। এই অনুমতিটি পপআপ মোডে খুলতে প্রয়োজন - রিক্যাপচা reCAPTCHA চ্যালেঞ্জ reCAPTCHA চ্যালেঞ্জ অনুরোধ করা হয়েছে - - কি:\\nঅনুরোধ:\\nকন্টেন্ট ভাষা:\\nসার্ভিস:\\nসময়(GMT এ):\\nপ্যাকেজ:\\nসংস্করণ:\\nওএস সংস্করণ:\\nআইপি পরিসর: -Stream file ডাউনলোড করুন। - তথ্য দেখান - + স্ট্রিম ফাইল ডাউনলোড করুন। + তথ্য দেখুন কি নতুন - যুক্ত করুন - খোজ ইতিহাস ইতিহাস সেবা @@ -189,25 +167,23 @@ প্লেলিস্ট সবসময় একবার মাত্র - [অজানা] - কোন ভিউ নেই নাম পরিবর্তন করুন - সেটিংস ওয়েব সাইট খুলুন ওয়েব সাইট - "কোন স্ট্রিম প্লেয়ার পাওয়া যায়নি (প্লে করতে VLC ইন্সটল করতে পারেন) " - যদি এই অপশন অন থাকে তবে কিছু ভিডিওর অডিও কাজ নাও করতে পারে + কোন স্ট্রিম প্লেয়ার পাওয়া যায়নি (প্লে করতে VLC ইন্সটল করতে পারেন) + কিছু রেজোলিউশনে ভিডিওর অডিও কাজ করে না সাবস্ক্রাইব সাবস্ক্রাইব করা আছে - চ্যানেল সাবস্ক্রাইব করা হয়েছে - সাবস্ক্রিপশন পরিবর্তন করা হয়নি - সাবস্ক্রিপশন আপডেট করতে ব্যার্থ হয়েছে + চ্যানেল থেকে আনসাবস্ক্রাইব্ড + সাবস্ক্রিপশন পরিবর্তন করা যায়নি + সাবস্ক্রিপশন আপডেটে ব্যার্থ প্রধান সাবস্ক্রিপশন - বুকমার্কস - + বুকমার্ককৃত প্লেলিস্টসমূহ দ্রুত টানা ব্যাবহার করুন - + আনসাবস্ক্রাইব + নতুন ট্যাব + \ No newline at end of file From 9bcccc87e6d585c3142438bf5b1af32be7c293d9 Mon Sep 17 00:00:00 2001 From: TobiGr Date: Tue, 26 Mar 2019 23:35:49 +0100 Subject: [PATCH 039/138] Update README Add supported services section Move comment support --- README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6c1aa3d4b..d4952b2b8 100644 --- a/README.md +++ b/README.md @@ -66,15 +66,22 @@ NewPipe does not use any Google framework libraries, nor the YouTube API. Websit * Enqueue videos * Local playlists * Subtitles -* Multi-service support (e.g. SoundCloud \[beta\]) * Livestream support +* Show comments ### Coming Features * Cast to UPnP and Cast -* Show comments * … and many more +### Supported Services + +NewPipe supports multiple services. Our [docs](https://teamnewpipe.github.io/documentation/) provide more info on how a new service can be added to the app and the extractor. Please get in touch with us if you intend to add a new one. Currently supported services are: + +* YouTube +* SoundCloud \[beta\] +* media.ccc.de \[beta\] + ## Updates When a change to the NewPipe code occurs (due to either adding features or bug fixing), eventually a release will occur. These are in the format x.xx.x . In order to get this new version, you can: * Build a debug APK yourself. This is the fastest way to get new features on your device, but is much more complicated, so we recommend using one of the other methods. From be6bce57711a3d3f32dc4aaaf8640643f5d765a9 Mon Sep 17 00:00:00 2001 From: sherlockbeard Date: Tue, 26 Mar 2019 19:13:38 +0000 Subject: [PATCH 040/138] Translated using Weblate (Hindi) Currently translated at 74.3% (329 of 443 strings) --- app/src/main/res/values-hi/strings.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 4d7397152..06f53dcd6 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -61,7 +61,7 @@ अन्य ऑडियो प्लेयर उपयोग करें मुख्य "सदस्यता को बदलने में असमर्थ " - सदस्यता को अपडेट करने में असमर्थ है + सदस्यता को अद्यतित नहीं किया जा सका देखे की क्या नया है विडियो को डाउनलोड करने के लिए फाइल की जगह डाउनलोड किए गए विडियो फाइल को रखने की जगह @@ -330,7 +330,7 @@ छायाप्रारुप लोड करें तेजी से अचूक तलाश का प्रयोग करें अचूक खोज प्लेयर को कम परिशुद्धता के साथ तेजी से पदों की तलाश करने की अनुमति देता है - बंद होने पे छोटी छवियां नहीं दिखेंगी। इससे डेटा और मेमोरी की बचत होगी। इसे बदलने से मेमोरी और डिस्क की छवि का भंडार भी साफ़ हो जाएगा। + लोडिंग थंबनेल, डेटा और मेमोरी उपयोग को रोकने के लिए बंद करें। इन-मेमोरी और ऑन-डिस्क छवि कैश दोनों को बदलता है छवि कैश मिटा दिया कैश मेटाडेटा वाइप करें सभी कैश किए गए वेबपृष्ठ डेटा हटाएं @@ -406,4 +406,5 @@ सदस्यता वापस ले ली नया टॅब टॅब चुने + वॉल्यूम नियंत्रण \ No newline at end of file From c4d451e42011815bd08b39d024aac2fa87d8558d Mon Sep 17 00:00:00 2001 From: Igor Nedoboy Date: Thu, 28 Mar 2019 23:21:23 +0000 Subject: [PATCH 041/138] Translated using Weblate (Russian) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-ru/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index e83b16772..b151a0f23 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -49,7 +49,7 @@ Путь загрузки аудио Папка для хранения загруженных аудио Введите путь к папке для загрузки аудио - Коснитесь поиска, чтобы начать + Начните с поиска Подождите… Файл уже существует Потоки @@ -141,7 +141,7 @@ Лучшее разрешение Запрос reCAPTCHA Запрошен ввод reCAPTCHA - Показывать высокие разрешения + Высокие разрешения NewPipe в окне О NewPipe Настройки @@ -470,5 +470,5 @@ Не удалось соединиться с сервером Не удалось получить данные с сервера Пост-обработка не удалась - Останавливать скачивание при переходе на мобильную сеть + Приостановить в мобильной сети \ No newline at end of file From c69de107e5b98d6bf759dd4aa1e5efbbbfc2cfb4 Mon Sep 17 00:00:00 2001 From: Stjepan Date: Thu, 28 Mar 2019 13:15:29 +0000 Subject: [PATCH 042/138] Translated using Weblate (Croatian) Currently translated at 68.8% (305 of 443 strings) --- app/src/main/res/values-hr/strings.xml | 131 ++++++++----------------- 1 file changed, 42 insertions(+), 89 deletions(-) diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index d6972a210..c0cd0cdd1 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -1,5 +1,6 @@ - -Dodirnite pretragu za početak + + + Dodirnite pretragu za početak %1$s pregleda Objavljeno %1$s Reproduktor za stream nije pronađen. Želite li instalirati VLC? @@ -24,23 +25,17 @@ Pretplata na kanalu otkazana Nije moguće promijeniti pretplatu Nije moguće osvježiti pretplatu - Početna Pretplate - Što je novo - Pozadina Skočni prozor - Put za preuzimanje videozapisa Put za spremanje videozapisa u Unesi put za preuzimanje videozapisa - Put za preuzimanje zvuka Put za spremanje preuzetog zvuka u Unesi put za preuzimanje zvučne datoteke - Auto. reprod. kada je NewPipe pozvan iz druge apl. Reproducira videozapis kad je NewPipe pozvan iz druge aplikacije Zadana razlučivost @@ -70,10 +65,7 @@ Pratite pogledane videozapise Nastavi nakon dobivanja fokusa Nastavi reproducirati nakon prekidanja (npr. telefonski pozivi) - - Preuzmi - Sljedeći videozapis Prikaži \'sljedeće\' i \'slične\' videozapise URL nije podržan @@ -102,7 +94,6 @@ Očisti Mijenjanje veličine Najbolja razlučivost - Greška Greška u mreži Nije moguće učitati sve ikone @@ -125,8 +116,6 @@ Što:\\nRequest:\\nContent Jezik:\\nService:\\nGMT Vrijeme:\\nPackage:\\nVersion:\\nOS version: Vaš komentar (na engleskom): Detalji: - - Sličica pregleda videozapisa Sličica pregleda videozapisa Profilna slika prenositelja @@ -136,31 +125,24 @@ (Eksperimentalno) Prisili promet preuzimanja kroz Tor radi povećane privatnosti (streamanje videozapisa još nije podržano). Prijavi grešku Korisničke prijave - Nije moguće napraviti direktorij za preuzimanje \'%1$s\' Napravljen direktorij za preuzimanje \'%1$s\' - Videozapis Zvuk Ponovno pokušaj Dozvola za pisanje po pohrani je odbijena Koristi stari reproduktor Stari ugrađeni Mediaframework reproduktor - - tis mil mlrd - Počni Pauziraj Reproduciraj Izbriši Kontrolna suma - Novi zadatak U redu - Naziv datoteke Niti Greška @@ -174,19 +156,15 @@ Molimo odaberite dostupnu mapu za preuzimanje Ova dozvola je potrebna za \notvaranje skočnog prozora - reCAPTCHA reCAPTCHA zadatak Traži se reCAPTCHA zadatak - Preuzimanja Dozvoljeni znakovi u nazivima datoteka Nedozvoljeni znakovi su zamjenjeni ovima Znak za zamjenu - Slova i brojevi Posebni znakovi - O NewPipeu Postavke O aplikaciji @@ -203,7 +181,6 @@ Ako imate ideja za prijevod, promjene u dizajnu, čišćenje koda ili neke veće promjene u kodu, pomoć je uvijek dobro došla. Što više radimo, to bolji postajemo! Pročitaj licencu Doprinos - Povijest Pretraživano Gledano @@ -211,53 +188,43 @@ Povijest Povijest je prazna Povijest očišćena - -NewPipe obavijest + NewPipe obavijest Obavijesti za NewPipe pozadinske i skočne reproduktore - Reproduktor Ponašanje Povijest Popis naslova Poništi - Nema rezultata Ovdje nema ništa osim cvrčaka - Nema pretplatnika - %s pretplatnik - %s pretplatnika - %s pretplatnika - - + %s pretplatnik + %s pretplatnika + %s pretplatnika + Nema pregleda - %s pregled - %s pregleda - %s pregledi - - + %s pregled + %s pregleda + %s pregledi + Nema videozapisa - %s video - %s videozapisa - %s videozapisi - - + %s video + %s videozapisa + %s videozapisi + Stavka je izbrisana -U redu čekanja za reprod. u pozadini + U redu čekanja za reprod. u pozadini Reproduciraj sve - Nije moguće reproducirati ovaj stream Dogodila se neoporavljiva pogreška reproduktora Oporavljanje od pogreške reproduktora - Prikaži savjet za držanje Prikažite savjet kada je pritisnut gumb za pozadinsku ili skočnu reprodukciju na stranici detalja videozapisa U redu čekanja za skočnu reprodukciju Želite li izbrisati ovu stavku iz povijesti pretraživanja? - Sadržaj Prazna stranica Kiosk stranica @@ -267,7 +234,6 @@ Odaberite kanal Niste pretplaćeni na nijedan kanal Odaberite kiosk - Nazivi kioska U trendu Vrh 50 @@ -278,8 +244,7 @@ Detalji Postavke zvuka Zadržite za dodavanje u red čekanja -[Nepoznato] - + [Nepoznato] Doniraj Web stranica Ovdje započni reprodukciju @@ -287,58 +252,43 @@ Dodaj na red čekanja u pozadini Dodaj na red u skočnom prozoru Započni ovdje u Skočnom prozoru - Otvori ladicu Zatvori ladicu Nešto će zasigurno doći ovdje :D - - Video reprodukcija Pozadinska reprodukcija Skočna reprodukcija Uvjek pitaj - Dohvaćam informacije… Odabrani sadržaj se učitava - Napravi novu reprodukcijsku listu Izbriši reprodukcijsku listu Preimenuj reprodukcijsku listu Ime liste Dodaj na reprodukcijsku listu Postavi kao sliku na listu - Markirajte reprodukcijsku listu "Odmarkirajte " - Želite li izbrisati listu? Reprodukcijska lista je kreirana Dodano na listuu Slika liste se promjenila Greška prilikom brisanja liste - Bez naslova - Podesno Ispuniti Povećaj - Auto generirano Veličina fonta naslova Manji font Normalni font Veći font - Omogući \"LeakCanary\" Monitoring curenja memorije može uzrokovati greške u radu aplikacije prilikom odlaganje gomile - Izvijestite o pogreškama izvan životnog ciklusa Prikaži informacije - Oznake - Dodaj u - Učitaj slike Slikovna predmemorija obrisana Obriši predmemorijsku metupodataka @@ -350,11 +300,9 @@ Uvijek Samo jednom Datoteka - Prijeđi na pozadinu Prijeđi na skočni prozor Prijeđi na glavni - Uvoz baze podataka Izvoz baze podataka Poništava vašu trenutnu povijest i pretplate @@ -371,17 +319,13 @@ Naziv datoteke ne može biti prazan Dogodila se greška: %1$s Povucite za promjenu redoslijeda - Stvori Izbriši jedan Izbriši sve Odbaci Preimenuj - 1 stavka izbrisana. - Nijedna aplikacija nije instalirana za reprodukciju te datoteke - Vrati Posjetite web stranicu NewPipe za više informacija i vijesti. NewPipeova pravila o privatnosti @@ -390,47 +334,37 @@ Jeste li sigurni da želite izbrisati sve stavke iz povijesti? Zadnje svirano Najviše svirano - Izvoz završen Uvoz završen Nema važeće ZIP datoteke Upozorenje: Nije moguće uvesti sve datoteke. Ovo će poništiti vaše trenutne postavke. Želite li također uvesti postavke? - Uvoz/Izvoz Uvoz Uvoz iz Izvoz u - Uvoz… Izvoz… - Uvoz datoteke Prethodni izvozi - Nije moguće uvesti pretplatnike Nije moguće izvesti pretplatnike - - Uvezite YouTube pretplatnike preuzimanjem izvozne datoteke: -\n -\n1. Idite na ovaj URL: %1%s -\n2. Ulogirajte se + Uvezite YouTube pretplatnike preuzimanjem izvozne datoteke: +\n +\n1. Idite na ovaj URL: %1$s +\n2. Ulogirajte se \n3. Preuzimanje bi trebalo početi (to je izvozna datoteka) vašID, soundcloud.com/vašID - Uzmite u obzir da ova operacija može uzrokovat veliku potrošnju prometa. \n \nŽelite li nastaviti? - Kontrole brzine reprodukcije Premotaj naprijed tijekom šutnje Korak Resetiraj - Prihvati Odbij - Bez ograničenja Ograniči rezoluciju tijekom korištenja mobilnih podataka Nijedan @@ -438,4 +372,23 @@ Preuzmite datoteku za stream. Koristi brzo netočno premotavanje Netočno premotavanje omogućava reproduktoru da premota na mjesto brže uz manju preciznost - + Otkaži pretplatu + Nova kartica + Odaberi karticu + Ažuriranja + Događaji + Datoteka obrisana + Obavijest za novu verziju NewPipe-a + Briše povijest ključnih riječi pretraživanja + Vanjska pohrana nije dostupna + Ažuriranja + Prikažite obavijest kada je dostupna nova verzija aplikacije + Lista + Rešetka + Promijeni prikaz + NewPipe dostupno ažuriranje! + Dodirnite za preuzimanje + Preuzimanje nije uspjelo + Preuzimanje gotovo + Prikaži pogrešku + \ No newline at end of file From 9b6924ec9fc34c591904a6296c140adb4cc72204 Mon Sep 17 00:00:00 2001 From: Igor Nedoboy Date: Thu, 28 Mar 2019 23:39:16 +0000 Subject: [PATCH 043/138] Translated using Weblate (Russian) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-ru/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index b151a0f23..260b73d2b 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -458,7 +458,7 @@ События Конференции Завершающая обработка - На очереди + В очереди Загрузка завершена " %s загрузок завершено" Создать уникальное имя @@ -466,7 +466,7 @@ Максимум попыток Максимальное число попыток перед отменой загрузки Загрузки, которые невозможно приостановить, будут перезапущены - Не удалось установить защищенное соединение + Не удалось установить защищённое соединение Не удалось соединиться с сервером Не удалось получить данные с сервера Пост-обработка не удалась From 46b8bdace77cf3dc6a2fdbb342dc49fcf39e76f0 Mon Sep 17 00:00:00 2001 From: Stjepan Date: Fri, 29 Mar 2019 16:03:05 +0000 Subject: [PATCH 044/138] Translated using Weblate (Croatian) Currently translated at 99.3% (440 of 443 strings) --- app/src/main/res/values-hr/strings.xml | 131 ++++++++++++++++++++----- 1 file changed, 105 insertions(+), 26 deletions(-) diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index c0cd0cdd1..9a9bbe004 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -17,7 +17,7 @@ Izaberi pretraživač rotacija Koristi vanjski reproduktor videozapisa - Neke razlučivosti NEĆE imati zvuk kad je ova opcija omogućena + Uklanja zvuk na NEKIM rezolucijama Koristi vanjski reproduktor za zvuk NewPipe skočni prozor Pretplati se @@ -33,8 +33,8 @@ Put za preuzimanje videozapisa Put za spremanje videozapisa u Unesi put za preuzimanje videozapisa - Put za preuzimanje zvuka - Put za spremanje preuzetog zvuka u + Mapa za preuzimanje zvuka + Preuzeti zvuk je spremljen ovdje Unesi put za preuzimanje zvučne datoteke Auto. reprod. kada je NewPipe pozvan iz druge apl. Reproducira videozapis kad je NewPipe pozvan iz druge aplikacije @@ -61,13 +61,13 @@ Prikaži prijedloge pri traženju Povijest pretraživanja Svaku pretragu spremi lokalno - Povijest + Povijest & Predmemorija Pratite pogledane videozapise Nastavi nakon dobivanja fokusa Nastavi reproducirati nakon prekidanja (npr. telefonski pozivi) Preuzmi - Sljedeći videozapis - Prikaži \'sljedeće\' i \'slične\' videozapise + Sljedeće + Prikaži \'Sljedeće\' i \'Slične\' videozapise URL nije podržan Zadani jezik sadržaja Video i zvuk @@ -80,7 +80,7 @@ Sadržaj Prikaži eksplicitni sadržaj Videozapis je dobno ograničen. Dopuštanje takvog sadržaja moguće je u postavkama. - uživo + UŽIVO Preuzimanja Preuzimanja Prijavi grešku @@ -103,7 +103,7 @@ Sadržaj nije dostupan Blokirano od GEMA-e Nije moguće postaviti izbornik za preuzimanje - Ovo je PRIJENOS UŽIVO, koji još nije podržan. + Prijenos uživo još nije podržan Nije moguće dobaviti stream Nije moguće učitati sliku Aplikacija/UI se srušio @@ -184,7 +184,7 @@ Povijest Pretraživano Gledano - Povijest ugašena + Povijest je ugašena Povijest Povijest je prazna Povijest očišćena @@ -192,7 +192,7 @@ Obavijesti za NewPipe pozadinske i skočne reproduktore Reproduktor Ponašanje - Povijest + Povijest & predmemorija Popis naslova Poništi Nema rezultata @@ -238,8 +238,8 @@ U trendu Vrh 50 Novo i popularno - Lista čekanja - Skočni reproduktcija + Pozadinski player + Skočni reproduktor Ukloni Detalji Postavke zvuka @@ -250,20 +250,20 @@ Ovdje započni reprodukciju Ovdje započni repr. u pozadini Dodaj na red čekanja u pozadini - Dodaj na red u skočnom prozoru + Dodaj na red u novom skočnom prozoru Započni ovdje u Skočnom prozoru Otvori ladicu Zatvori ladicu - Nešto će zasigurno doći ovdje :D + Nešto će se uskoro pojaviti :D Video reprodukcija Pozadinska reprodukcija Skočna reprodukcija Uvjek pitaj Dohvaćam informacije… Odabrani sadržaj se učitava - Napravi novu reprodukcijsku listu - Izbriši reprodukcijsku listu - Preimenuj reprodukcijsku listu + Nova reprodukcijska lista + Izbriši + Preimenuj Ime liste Dodaj na reprodukcijsku listu Postavi kao sliku na listu @@ -271,9 +271,9 @@ "Odmarkirajte " Želite li izbrisati listu? Reprodukcijska lista je kreirana - Dodano na listuu - Slika liste se promjenila - Greška prilikom brisanja liste + Dodano na listu + Slika liste se promjenila. + Greška prilikom brisanja liste. Bez naslova Podesno Ispuniti @@ -287,7 +287,7 @@ Monitoring curenja memorije može uzrokovati greške u radu aplikacije prilikom odlaganje gomile Izvijestite o pogreškama izvan životnog ciklusa Prikaži informacije - Oznake + Označene Liste za reprodukciju Dodaj u Učitaj slike Slikovna predmemorija obrisana @@ -309,13 +309,13 @@ Izvoz povijesti, pretplata i playlisti Očisti povijest gledanja Briše povijest reproduciranih streamova - Obriši cijelu povijest gledanja. + Obriši cijelu povijest gledanja\? Povijest gledanja izbrisana. Obriši povijest pretraživanja - Obriši cijelu povijest pretraživanja. + Obriši cijelu povijest pretraživanja\? Povijest pretraživanja obrisana. Neispravan URL - Nevažeći direktorij + Nema takve mape Naziv datoteke ne može biti prazan Dogodila se greška: %1$s Povucite za promjenu redoslijeda @@ -368,8 +368,8 @@ Bez ograničenja Ograniči rezoluciju tijekom korištenja mobilnih podataka Nijedan - Reproduktor za stream nije pronađen (možete instalirati VLC za reprodukciju) - Preuzmite datoteku za stream. + Reproduktor za stream nije pronađen (možete instalirati VLC za reprodukciju). + Preuzmite datoteku za stream Koristi brzo netočno premotavanje Netočno premotavanje omogućava reproduktoru da premota na mjesto brže uz manju preciznost Otkaži pretplatu @@ -391,4 +391,83 @@ Preuzimanje nije uspjelo Preuzimanje gotovo Prikaži pogrešku + Isključite kako biste spriječili učitavanje sličica, spremanje podataka i korištenja memorije. Promjene čiste predmemoriju i predmemoriju slika. + Uklonite sve podatke iz privremenih web-stranica + Metapodaci su izbrisani + Automatski dodaj u red sljedeće strujanje + Automatsko dodavanje povezanog videozapisa tijekom reprodukcije posljednjeg videozapisa u neponavljajućem redu. + Kontrola glasnoće pomoću gesti + Koristi gesture za kontrolu glasnoće + Kontrola svjetline pomoću gesti + Koristi gesture za kontrolu svjetline + Zadana zemlja sadržaja + Otkrivanje grešaka + Obavijest o ažuriranju aplikacije + Uključite ili isključite orijentaciju + Preuzimanje na vanjsku SD karticu još nije moguće. Poništite lokaciju mape za preuzimanje\? + Vanjski playeri ne podržavaju ove vrste veza + Nije pronađen nijedan videozapis + Nije pronađen nijedan zvuk + Nema takve datoteke/izvora sadržaja + Datoteka ne postoji ili joj nedostaje dopuštenje za čitanje ili pisanje + Nema dostupnih videozapisa za preuzimanje + Pomoću zadanih kartica pojavljuje se pogreška prilikom čitanja spremljenih kartica + Vratiti zadane + Želite li vratiti zadane postavke\? + Broj pretplatnika nije dostupan + NewPipe razvijaju volonteri koji provode vrijeme donoseći vam najbolje iskustvo. Vratite im kako biste programerima učinili da NewPipe bude još bolji dok uživate u šalici kave. + Koje su kartice prikazane na glavnoj stranici + Izbor + Konferencije + Preferirana \'otvori\' akcija + Zadana radnja pri otvaranju sadržaja — %s + Podnaslovi + Izmijenite skalu teksta naslova player-a i pozadinske stilove. Potrebno je ponovno pokretanje aplikacije kako bi stupilo na snagu. + Prisilno izvješćivanje o greškama Rx-a koje se ne mogu isporučiti izvan \'fragmenta\' ili životnog ciklusa aktivnosti nakon odlaganja + Uvezite SoundCloud profil tako da upišete URL ili svoj ID: +\n +\n1. Omogućite \"način rada na radnoj površini\" u web-pregledniku (stranica nije dostupna na mobilnim uređajima) +\n2. Idite na ovaj URL: %1$s +\n3. Ulogirajte se +\n4. Kopirajte URL profila na koji ste preusmjereni. + Tempo + Visina tona + Prekini vezu (može uzrokovati izobličenje) + Minimiziraj prilikom mjenjanje aplikacija + Radnja prilikom prebacivanja na drugu aplikaciju iz glavnog videoplayer-a — %s + Minimiziraj na pozadinski player + Minimiziraj na skočni player + Način prikaza popisa + Automatski + Gotovo + U redu za čekanje + pauzirano + Na redu za čekanje + naknadna obrada + Red + Sustav je odbio radnju + %s preuzimanja dovršeno + Generirajte jedinstveni naziv + Piši preko + Preuzeta datoteka s tim nazivom već postoji + U tijeku je preuzimanje s ovim nazivom + Kod + Datoteku nije moguće izraditi + Odredišnu mapu nije moguće izraditi + Sustav je odbio dozvolu + Sigurna veza nije uspjela + Nije moguće pronaći server + Nije moguće povezati se s serverom + Server ne šalje podatke + Poslužitelj ne prihvaća preuzimanja s više niti, pokušaj ponovo s @string/msg_threads = 1 + Traženi raspon nije zadovoljavajući + Nije pronađeno + Naknadna obrada nije uspjela + Obriši završena preuzimanja + Nastavite s prijenosima na čekanju za %s s preuzimanja + Stop + Maksimalnih ponovnih pokušaja + Maksimalni broj pokušaja prije poništavanja preuzimanja + Pauziraj prilikom prebacivanja na mobilne podatke + Preuzimanja koja se ne mogu zaustaviti ponovno će se pokrenuti \ No newline at end of file From 29183c10fff1af9661a3aadc271847a3048d4b40 Mon Sep 17 00:00:00 2001 From: Florian Date: Sun, 31 Mar 2019 14:20:01 +0000 Subject: [PATCH 045/138] Translated using Weblate (French) Currently translated at 99.1% (439 of 443 strings) --- app/src/main/res/values-fr/strings.xml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index c6aa99bfb..bc63d927f 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -318,7 +318,7 @@ Recherche rapide approximative Permettre au lecteur d\'accéder plus rapidement à une position au détriment de la précision Charger les miniatures - Désactivez pour empêcher le chargement des miniatures afin de réduire l’utilisation de bande passante et de mémoire. Modifier cette option vide le cache d’images en mémoire vive et sur le disque. + Désactivez pour empêcher le chargement des miniatures afin de réduire l’utilisation de bande passante et de mémoire. Ce changement vide le cache d’images en mémoire vive et sur le disque. Images en cache effacées Effacer les données en cache Effacer toutes les pages web mises en cache @@ -464,4 +464,11 @@ Effacer les téléchargements terminés Mettre en pause lors du passage en données mobiles Les téléchargements qui ne peuvent pas être mis en pause seront redémarrés + Mode liste + post-traitement + File d’attente + Action refusée par le système + Échec du post-traitement + Nombre maximum de tentatives + Nombre maximum de tentatives avant d\'annuler le téléchargement \ No newline at end of file From e5f3b2bf6e397ef2082e2e9e0d50522b3cdc7c36 Mon Sep 17 00:00:00 2001 From: nautilusx Date: Sat, 30 Mar 2019 14:30:57 +0000 Subject: [PATCH 046/138] Translated using Weblate (German) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-de/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 70759173c..452fd01bf 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -80,7 +80,7 @@ Dein Kommentar (auf englisch): Konnte keinen Stream abrufen Automatische Wiedergabe - Spielt ein Video ab, wenn NewPipe von einer anderen App aufgerufen wurde + Wiedergabe eines Videos, wenn NewPipe von einer anderen App aufgerufen wurde Einen Fehler melden Anwenderbericht LIVE @@ -463,7 +463,7 @@ Kann nicht mit dem Server verbinden Der Server sendet keine Daten Der Server erlaubt kein mehrfädiges Herunterladen – wiederhole mit @string/msg_threads = 1 - Angefragter Bereich nicht bedienbar + Gewünschter Bereich ist nicht verfügbar Nicht gefunden Nachbearbeitung fehlgeschlagen Um fertige Downloads bereinigen From 0a1e7a7c864f13763d2d95d49a242e1d6bb36cfe Mon Sep 17 00:00:00 2001 From: Marco Ieni Date: Sat, 30 Mar 2019 22:32:53 +0000 Subject: [PATCH 047/138] Translated using Weblate (Italian) Currently translated at 99.3% (440 of 443 strings) --- app/src/main/res/values-it/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index e75d4f89c..b53e82d54 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -62,7 +62,7 @@ I contenuti in diretta non sono al momento supportati Contenuti Contenuti vietati ai minori - Mostra video riversati a un pubblico maggiorenne. Si possono abilitare dalle Impostazioni. + Mostra video riservati a un pubblico maggiorenne. Si possono abilitare dalle Impostazioni. Tocca Cerca per iniziare Riproduzione automatica Riproduci i video quando NewPipe viene aperto da un\'altra app From 1e4b1a2c70bb1ecfa7f121ac58577893f8c18a92 Mon Sep 17 00:00:00 2001 From: Igor Nedoboy Date: Sun, 31 Mar 2019 20:17:28 +0000 Subject: [PATCH 048/138] Translated using Weblate (Russian) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-ru/strings.xml | 34 +++++++++++++------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 260b73d2b..ce488455c 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -7,24 +7,24 @@ Отмена Открыть в браузере Поделиться - Скачать + Загрузить Поиск Настройки Возможно, вы имели в виду: %1$s? Поделиться в - Выбрать браузер + Выбор браузера поворот Путь загрузки видео Путь для сохранения загруженных видео Введите путь к папке для загрузки видео - Стандартное качество + Разрешение по умолчанию Воспроизвести в Kodi Приложение Kore не найдено. Установить его? \"Воспроизвести в Kodi\" Показать опцию воспроизведения через медиацентр Kodi Аудио Формат аудио по умолчанию - Скачать + Загрузить Следующее видео URL не поддерживается \"Следующее\" и \"Похожие\" видео @@ -61,7 +61,7 @@ Имя файла Ошибка Сервер не поддерживается - NewPipe скачивает + NewPipe загружает Неверный URL или нет доступа к интернету Подробнее Скопировано в буфер обмена @@ -273,7 +273,7 @@ Плеер в окне Получение сведений… Загрузка запрошенного контента - Скачать файл прямой трансляции + Загрузить файл прямой трансляции Показать сведения Закладки Добавить к @@ -432,37 +432,37 @@ Уведомление об обновлении Уведомления о новой версии NewPipe Обновления - Показывать уведомление с подтверждением обновления, когда доступна новая версия - Доступна новая версия NewPipe! - Нажмите, чтобы скачать + Показать уведомление с предложением обновить приложение при наличии новой версии + Доступно обновление NewPipe! + Нажмите для загрузки Завершено приостановлено - Добавлено в очередь + в очереди Очередь Действие запрещено системой - Ошибка скачивания + Ошибка загрузки Перезаписать Файл с таким именем уже существует - Файл с таким именем уже скачивается + Загрузка с таким именем уже выполняется Показать текст ошибки Код Файл не может быть создан - Целевая папка не может быть создана + Папка назначения не может быть создана Доступ запрещен системой Сервер не найден - Сервер не поддерживает многопотоковое скачивание, попробуйте с @string/msg_threads = 1 + "Сервер не поддерживает многопотоковую загрузку, попробуйте с @string/msg_threads = 1" Запрашиваемый диапазон недопустим Не найдено - Очистить завершенные + Очистить завершённые Остановить События Конференции - Завершающая обработка + пост-обработка В очереди Загрузка завершена " %s загрузок завершено" Создать уникальное имя - Продолжить ожидающие загрузки (%s) + Возобновить приостановленные загрузки (%s) Максимум попыток Максимальное число попыток перед отменой загрузки Загрузки, которые невозможно приостановить, будут перезапущены From 8232a92653e80ceb317bdfc7086d05045b28b3a0 Mon Sep 17 00:00:00 2001 From: Tobias Groza Date: Tue, 2 Apr 2019 14:30:54 +0000 Subject: [PATCH 049/138] Translated using Weblate (Romanian) Currently translated at 75.6% (335 of 443 strings) --- app/src/main/res/values-ro/strings.xml | 110 ++++++++----------------- 1 file changed, 34 insertions(+), 76 deletions(-) diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 8e9f83fdf..26dcce0c3 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -16,15 +16,12 @@ rotație Folosește un player video extern Folosește un player audio extern - Locația videoclipurilor descărcate Locul în care se vor descărca videoclipurile Introduceți locația în care se vor descărca videoclipurile - Directorul audio-ului descărcat Locul în care se va descărca audio-ul Introduceți locația în care se va descărca fişierele audio - Rezoluție implicită Redați folosind Kodi Aplicația Kore nu a fost găsită. Doriți să o instalați? @@ -36,7 +33,6 @@ Temă Întunecat Luminos - Descărcați Următorul Arată videoclipurile care urmează @@ -50,7 +46,6 @@ Conținut Conținut restricționat în funcție de vârstă Arată videoclipul restricționat în funcție de vârstă. Permiterea vizionării este posibilă din Setări. - Eroare Eroare de rețea Nu s-au putut încărca toate thumbnail-urile @@ -60,8 +55,6 @@ Blocat de către GEMA Imposibil de inițializat meniul pentru descărcări LIVE STREAM-uri încă nu sunt suportate - - Thumbnail-ul videoclipului Thumbnail de previzualizare al videoclipului Thumbnail-ul avatarului autorului @@ -69,7 +62,6 @@ Nu au apreciat Folosește Tor (Experimental) Forțează descărcarea traficului prin Tor pentru intimitate crescută (transmiterea în flux a videoclipurilor nu este deocamdată suportată). - Nu s-a putut crea directorul de descărcare \'%1$s\' Apăsați căutare pentru a începe Redare automată @@ -78,7 +70,6 @@ Descărcări Descărcări Raport de erori - Nu s-a putut analiza complet website-ul Nu s-a putut prelua niciun stream Scuze, asta n-ar fi trebui să se întâmple. @@ -89,26 +80,19 @@ Ce s-a întâmplat: Comentariul tău (în engleză): Detalii: - - Raportează o eroare Raportul utilizatorului - Video Audio Reîncearcă Permisiunea de a accesa mediul de stocare a fost refuzată - Start Pauză Redă Șterge Suma de control - Misiune nouă OK - - Numele fișierului Thread-uri Eroare @@ -120,68 +104,53 @@ Vă rugăm așteptați… Copiat în clipboard Vă rugăm alegeți un folder pentru descărcări - Deschide in modul popup Aceasta permisiune este necesara pentru a deschide în mod pop-up - ReCAPTCHA reCAPTCHA noua reCAPTCHA noua ceruta - NewPipe mod pop-up - "Rezoluție pop-up inițială " Afișează rezoluții mai mari Doar anumite dispozitive suportă redarea videoclipurilor 2K/4K Format video implicit Negru - Redare în mod pop-up Tot Canal Da Mai târziu Dezactivat - Nu s-a putut încărca imaginea App/UI eroare Ce:\\nCerere:\\nLimba Conținutului:\\nServiciu:\\nTimp GMT:\\nPachet:\\nVersiune:\\nVersiune OS: - - K mil. mld. - -Anumite rezoluții NU vor avea sunet atunci când această opțiune este activată + Anumite rezoluții NU vor avea sunet atunci când această opțiune este activată Fundal Pop-up - Reține dimensiunea și poziția pop-up-ului Reține ultima dimensiune și poziție a pop-up-ului Gesturi player Folosește gesturile pentru a controla luminozitatea și volumul player-ului Arată sugestii Arată sugestii în timpul căutării - Pop-up Filtru Reîmprospătează Curăță Redimensionare Rezoluție maximă - Abonează-te Abonat(ă) Canal dezabonat Nu s-a putut modifica abonamentul Nu s-a putut actualiza abonamentul - Principal Abonamente - Ce este nou - Istoric de căutări Stochează local căutările Istoric și cache @@ -193,42 +162,34 @@ pentru a deschide în mod pop-up Istoric și cache Playlist Anulează - Notificare NewPipe Notificări pentru playere de fundal și pop-up - Fără rezultate Nimic aici în afară de sunetul greierilor - Niciun abonat - %s abonat - %s abonați - %s abonați - - + %s abonat + %s abonați + %s abonați + Nicio vizionare - %s vizionare - %s vizionări - %s vizionări - - + %s vizionare + %s vizionări + %s vizionări + Niciun videoclip - %s videoclip - %s videoclipuri - %s videoclipuri - - + %s videoclip + %s videoclipuri + %s videoclipuri + Descarcă Caractere permise în numele fișierelor Caracterele invalide sunt înlocuite cu această valoare Caracter înlocuitor - Litere și cifre Caracterele cele mai speciale - Despre NewPipe Setări Despre @@ -245,7 +206,6 @@ pentru a deschide în mod pop-up Dacă aveți idei, doriți să traduceți, să schimbați design-ul, să curățați codul sau să schimbați codul, ajutorul este mereu bine primit. Cu cât mai mult, cu atât mai bine! Citiți licența Contribuie - Istoric Căutat Vizionat @@ -255,7 +215,7 @@ pentru a deschide în mod pop-up Istoric curățat Element șters Doriți să ștergeți acest element din istoricul de căutare? -Conținutul pagini principale + Conținutul pagini principale Pagină Goală Pagină Chioșc Pagină Abonări @@ -264,22 +224,18 @@ pentru a deschide în mod pop-up Alegeți un canal Nu v-ați abonat la niciun canal deocamdată Alegeți un chioșc - Chioșc Trenduri Top 50 Tendințe -Serviciu + Serviciu Adăugat în player de fundal Adăugat în player pop-up Niciun player pentru streaming găsit. (Totuși, puteți instala VLC). Descărcați fișierul de streaming Arată informații - Marcaje lista de vizionare - Adaugă La - Folosește parcurgerea inexactă Derularea inexactă permite player-ului să fie poziţionat mai rapid dar cu o precizie mai redusă Încarcă miniaturi @@ -298,14 +254,11 @@ pentru a deschide în mod pop-up Întotdeauna Doar Odată Fișier - [Necunoscut] - Activează Orientarea Schimbă pe Modul fundal Schimbă pe modul popup Schimbă pe modul principal - Importează baza de date Exportează baza de date Va înlocui istoricul și abonamentele curente @@ -322,7 +275,6 @@ pentru a deschide în mod pop-up Fișierul nu există, sau nu dețineți suficiente permisiuni pentru a îl citi sau scrie Numele fișierului nu poate fi gol O eroare a apărut: %1$s - NewPipe este creat de dezvoltatori şi voluntari care îşi petrec timpul liber să vă aducă cea mai buna experienţă. Dați ceva înapoi pentru a ajuta dezvolatorii noştri în a pot face NewPipe şi mai bun în timp ce se bucură de o cană de cafea. A da înapoi Site-ul @@ -331,13 +283,11 @@ pentru a deschide în mod pop-up Sunteţi sigur că vreţi să ştergeţi toate elementele din istoric ? Ultimul cântec ascultat "Cel mai ascultat " - Exportat Importat Nici-un fişier ZIP valid Avertisment: Nu se pot importa fişierele. Acest lucru o să vă reseteze setup-ul curent. - Player în fundal Player popup Şterge @@ -349,40 +299,29 @@ pentru a deschide în mod pop-up Începeţi să redaţi de aici Începeţi de aici în Fundal Începeţi de aici în popup - Deschdeţi sertarul Închideţi sertarul Opţiunea de deschidere preferată Opţiunea prestabilită când se deschide contentul -%s - Player Video Player Fundal Player Popup Întreabă întotdeauna - Niciun stream disponbil pentru descărcare - Trageţi pentru a reordona - Crează Ştergeţi un element Ştergeţi toate elementele Ignoraţi "Redenumiţi " - Niciun player a fost găsit pentru acest fişier - Donaţi Ceva va apărea aici în curând ;D - - De asemenea, doriți să importați setări? - Nume Listă de redare creată Adăugat la lista de redare Generat automat - Import fișier Controlul vitezei de redare Canale @@ -408,4 +347,23 @@ pentru a deschide în mod pop-up Viteză Acceptă Refuză + Dezabonare + Panel nou + Alege panel + Gesturi control volum + Utilizează gesturi pentru controlul volumului + Gesturi control strălucire + Utilizează gesturi pentru controlul strălucire + Actualizări + Evenimente + Fișier șters + Notificare actualizare aplicație + Notificări pentru noi versiuni NewPipe + Afișare istoric șters. + Șterge istoric căutare + Ștergere istoric căutare cuvinte cheie + Șterge întregul istoric căutare\? + Istoric căutare șters + Stocare externă indisponibilă + Descărcarea pe cardul SD extern nu este încă posibilă. Se resetează locația directorului de descărcări\? \ No newline at end of file From de59bf695d841bf5b5017daf19c5eff7ee4424e6 Mon Sep 17 00:00:00 2001 From: ___ Date: Thu, 4 Apr 2019 06:12:39 +0000 Subject: [PATCH 050/138] Translated using Weblate (Ukrainian) Currently translated at 99.8% (442 of 443 strings) --- app/src/main/res/values-uk/strings.xml | 413 +++++++++++++------------ 1 file changed, 210 insertions(+), 203 deletions(-) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 81e25e7be..93dbaf48c 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -2,7 +2,7 @@ %1$s переглядів Опубліковано %1$s - Потокового програвача не знайдено. Чи бажаєте встановити VLC\? + Програвач потокового відео не знайдений. Бажаєте встановити VLC\? Встановити Скасувати Відкрити у переглядачеві @@ -11,59 +11,59 @@ Шукати Налаштування Чи ви мали на увазі: %1$s\? - Поділитись з + Поширити через Оберіть переглядач обертання - Зовнішній відео-програвач - Зовнішній авдіопрогравач - Шлях для завантаження відеозапису - Вкажіть шлях для завантаження відеозаписів - Вкажіть шлях для завантаження авдіофайлів - Шлях де зберігатимуться завантажені відеозаписи + Використовувати зовнішній відеопрогравач + Використовувати зовнішній аудіопрогравач + Місце збереження відеозаписів + Вкажіть шлях до теки для завантаження відео + Вкажіть шлях до теки для завантаження аудіо + Шлях до теки, де зберігатимуться завантажені відео Тека для завантаженого аудіо - Завантажені авдіофайли зберігатимуться тут - Автоматично відтворювати - Відтворює відеозапис коли NewPipe викликано з іншого застосунку + Завантажене аудіо зберігається тут + Автоматичне програвання + Програє відео коли NewPipe викликано з іншого додатку Типова роздільна здатність - Відтворювати за допомогою Kodi - Застосунок Kore не знайдено. Встановити? - Відтворювати у Kodi - Показувати опцію відтворення відеозапису за допомогою Kodi media center + Програти у Kodi + Додаток Kore не знайдено. Встановити його\? + Показати опцію \"Програти у Kodi\" + Показати опцію програвання відео у медіацентрі Kodi Аудіо - Типовий авдіоформат + Типовий формат аудіо Тема Темна Світла Завантажити - Наступний відеозапис - Являти \"наступні\" й \"схожі\" відео + Наступне відео + Показувати \"Наступне\" і \"Схожі\" відео URL не підтримується Переважна мова контенту Відео та Авдіо Зовнішній вигляд Інше - Відтворення у тловому режимі - Відтворити - У доступі відмовлено + Програвання у тлі + Програти + В доступі до сховища відмовлено Контент Контент з віковими обмеженнями - Показувати відеозаписи, які мають вікові обмеження. Спершу активуйте опцію для програвання таких записів у налаштуваннях. + Показувати відео з віковими обмеженнями. Дозволити програвання таких записів можна у Налаштуваннях. НАЖИВО Помилка Помилка мережі Не вдалося завантажити всі ескізи - Не вдалося розшифрувати URL підпис відеозапису + Не вдалося розшифрувати URL-підпис відео Не вдалося проаналізувати веб-сайт Не вдалося повністю проаналізувати веб-сайт Контент недоступний Заблоковано GEMA Не вдалося налаштувати меню завантаження - Трансляції НАЖИВО ще не підтримуються - Не вдалося отримати жодного стриму + Трансляції НАЖИВО поки що не підтримуються + Не вдалося отримати жодного потоку Шкода, цього не мало статися. - Повідомити про помилку за допомогою пошти - На жаль, трапилась помилка. - ПОВІДОМИТИ + Повідомити про помилку електронною поштою + На жаль, трапились деякі помилки. + ЗВІТ Інформація: Що сталося: Натисніть на пошук аби почати @@ -77,124 +77,124 @@ Пізніше Вимкнено Не вдалося завантажити зображення - Застосунок/інтерфейс зазнав краху + Додаток/інтерфейс впав Ваш коментар (англійською): - Деталі: + Подробиці: Ескіз попереднього перегляду відео Ескіз попереднього перегляду відео Використовувати Tor (Експериментально) Перенаправляти трафік через Tor для підвищення конфіденційності (трансляція відео ще не підтримується). Повідомити про помилку - Не вдалося створити теку для завантаження \'%1$s\' - Створити теку для завантаження \'%1$s\' + Не вдалося створити теку для завантажень \'%1$s\' + Створено теку для завантажень \'%1$s\' Відео Аудіо - Повторити + Повторити спробу Використовувати старий програвач - т - млн - мрд + тис. + млн. + млрд. Почати - Павза - Відтворити + Пауза + Грати Видалити Контрольна сума ОК - Назва файлу - Гілки + Ім\'я файлу + Потоки Помилка Сервер не підтримується - Файл уже існує + Файл вже існує NewPipe завантажує - Натисніть для подробиць - Будь ласка, зачекайте… - Копійовано до сховку - Будь ласка, оберіть теку для завантаження - Потокового програвача не знайдено (ви можете встановити VLC для відтворення). + Подробиці + Зачекайте… + Скопійовано до буферу обміну + Оберіть доступну теку для завантажень + Програвач потокового відео не знайдений (ви можете встановити VLC для програвання). Відкрити у віконному режимі - Усувається звук ПЕВНИХ роздільностей - NewPipe у віконному режимі + Прибирає звук при ПЕВНИХ роздільних здатностях + Віконний режим NewPipe Підписатися Ви підписалися Ви відписалися від каналу - Неможливо змінити підписання - Неможливо оновити підписання + Не вдалося змінити підписку + Не вдалося оновити підписку Головна - Підписання + Підписки Новинки - На тлі + У тлі У вікні Типова роздільна здатність вікна - Не всі пристрої підтримують відтворення 2K/4K відеозаписи - Вищі роздільні здатності - Типовий формат відеозапису - Пам\'ятати розмір та позицію вікна - Пам\'ятати останній розмір вікна - Керування жестами - Використовувати жести для контролю яскравості та гучності програвача - Шукати схожі + Лише деякі пристрої підтримують програвання 2K/4K-відео + Показувати вищі роздільні здатності + Типовий формат відео + Пам\'ятати розмір і позицію вікна + Пам\'ятати останній розмір і позицію вікна + Жести керування програвачем + Використовувати жести для контролю яскравості та гучності у програвачі + Пошукові пропозиції Показувати пропозиції під час пошуку Історія пошуків Зберігати пошукові запити локально Історія та кеш - Вести облік перегляду відеозаписів - Відновити відтворення - Продовжувати відтворення опісля переривання (наприклад телефонного дзвінка) - \"Утримуй для додавання\" підказка - Усталена країна контенту + Запам\'ятовувати переглянуті відео + Відновити програвання при поверненні з тла + Продовжувати програвання після переривань (наприклад, телефонних дзвінків) + Показати пораду \"Утримуй, щоб додати\" + Типова країна контенту Сервіс Програвач Поведінка Історія та кеш Вікно - Відтворення у вікні - Додано до тлового відтворення - Додано до черги у вікно - Плейлист - Фільтрувати + Програвання у віконному режимі + Додано до черги програвання у тлі + Додано до черги програвання у вікні + Список відтворення + Фільтр Оновити Очистити Зміна розміру - Найліпша роздільна здатність - Відміна - Грати всі + Найкраща роздільна здатність + Скасувати + Грати все Завжди - Тільки тепер - NewPipe сповіщення - "Сповіщення для NewPipe-ових тлового і віконного програвачів " + Лише раз + Сповіщення NewPipe + Сповіщення для програвачів NewPipe у тлі і у вікні [Невідомо] - Перемкнутися до тла - Перемкнутися до вікна - Перемкнутися на головну - Імпортувати базу - Експортувати базу - Перепише поточну історію та підписки - Експортувати історію, підписки та плейлисти - Неможливо відтворити цей стрим - Відновлююсь після помилки програвача - Помилкова URL - Відеостримів не знайдено - Не знайдено аудіопотоків + Перемкнути у тло + Перемкнути у вікно + Перемкнути на головну + Імпортувати базу даних + Експортувати базу даних + Перезапише вашу поточну історію та підписки + Експортувати історію, підписки та списки відтворення + Не вдалося програти цей потік + Відновлення після помилки програвача + Недійсний URL + Відеопотоки не знайдено + Аудіопотоки не знайдено Звіт користувача - Без підписників + Немає підписників Немає відео - Помилкова URL або інтернет недоступний - Цей дозвіл має бути відкритим -\nу вікні - «reCAPTCHA» + Помилковий URL або немає доступу в Інтернет + Цей дозвіл потрібен для перегляду +\nу віконному режимі + reCAPTCHA Завантаження - Допустимі символи у назвах файлів - "Неправильні символи коригуватимуться цим " - Символ заміни + Допустимі символи у іменах файлів + Недопустимі символи замінити на цей + Символ для заміни Літери та цифри - Більш специфічні символи + Більшість спеціальних символів Про NewPipe Налаштування - Візитівка - Неможливо завантажити ліцензію + Про додаток + Не вдалося завантажити ліцензію Ліцензії - Зробити внесок - Переглянути на Ґітгаб + Посприяти + Переглянути на GitHub Фінансова допомога Веб-сайт Історія @@ -202,136 +202,136 @@ Експортовано Імпортовано Топ 50 - Новинки - Деталі - Звукові налаштування + Нове і гаряче + Подробиці + Налаштування аудіо Відеопрогравач Тловий програвач Віконний програвач - Отримую інформацію… - Завантажується запитаний контент + Отримання інформації… + Завантаження запитаного контенту Завантажити потоковий файл Показати інформацію - Закладкові плейлисти + Закладені списки відтворення Додати до - Показувати підказку коли натиснута кнопка тла або вікна на сторінці інформації відеозапису + Показати пораду, коли натиснута кнопка \"У тло\" або \"У вікно\" на сторінці з подробицями відео Перемкнути орієнтацію - Фатальна помилка програвача + Сталася невиправна помилка програвача Зовнішні програвачі не підтримують такі види посилань - Що:\\nЗапит:\\nМова змісту:\\nСервіс:\\nЧас GMT:\\nПакунок:\\nВерсія:\\nВерсія ОС: + Що:\\nЗапит:\\nМова контенту:\\nСервіс:\\nЧас GMT:\\nПакунок:\\nВерсія:\\nВерсія ОС: Ескіз аватару користувача Сподобалося Не сподобалося Нічого не знайдено - Перетягніть для зміни сортування + Перетягуйте для впорядкування %s підписник %s підписники - n > 1 + %s підписників - Відеозапис - Відеозаписи - Відеозаписів + Відео + Відео + Відео Створити - Видалення одного - Видалити всі + Видалити одне + Видалити все Відхилити - Змінити назву - Сторонні ліцензії - Перейти до сайту - Візитівка - Вільний та легковагий стримінґ для Андроїду. - Які б не були Ваші ідеї: переклад, дизайн, легкий чи глобальний рефакторинґ - будь-яка допомога завжди у нагоді. Що більше зроблено, то ліпшим NewPipe стає! - Усунення вад - Нічого нема - Перегляди відсутні + Перейменувати + Ліцензії третіх сторін + Відкрити веб-сайт + Про додаток + Вільне та легке потокове програвання на Android. + Які б не були Ваші ідеї: переклад, дизайн, легкий чи глобальний рефакторинг - будь-яка допомога завжди у нагоді. Що більше зроблено, то ліпшим стає NewPipe! + Налагодження + Нічого немає... чути лише цвіркунів + Немає переглядів %s перегляд %s перегляди %s переглядів Нове завдання - Виклик reCAPTCHA - Запит на виклик reCAPTCHA - © %1$s by %2$s under %3$s + Перевірка reCAPTCHA + Запит на перевірку reCAPTCHA + © %1$s, %2$s під %3$s Учасники - NewPipe розроблений добровольцями, які витрачають власний час заради вашого задоволення. Допоможіть розробникам зробити NewPipe ще ліпшим, насолоджуючись філіжанкою кави. + NewPipe розроблений добровольцями, які витрачають власний час заради вашого задоволення. Допоможіть розробникам зробити NewPipe ще кращим, насолоджуючись філіжанкою кави. Ваша допомога - Завітайте на офіційний сайт, аби отримати більше інформацій та новин. + Завітайте на офіційний сайт NewPipe, аби отримати більше інформації та новин. Ліцензія NewPipe Прочитати ліцензію Історія пошуку - Переглянуто + Історія перегляду Історію вимкнено Історія Історія відсутня Елемент видалено - Чи ви хочете видалити цей елемент з історії пошуку\? - Чи ви хочете видалити цей елемент з історії переглядів\? - Чи ви впевнені, що хочете видали всі елементи з історії\? + Видалити цей елемент з історії пошуку\? + Видалити цей елемент з історії перегляду\? + Дійсно видалити всі елементи з історії\? Програвалося останнім Програвалося найбільше Контент на головній сторінці Порожня сторінка - Kiosk - Підписання - Нове - Сторінка каналу + Кіоск-сторінка + Підписки + Потоки + Канал Обрати канал Немає підписок на канали - Обрати kiosk - Недійсний ZIP-файл - Увага: не можливо здійснити імпортування всіх файлів. - Це перепише ваші поточні налаштування. + Обрати кіоск + Немає дійсного ZIP-файлу + Увага: не вдалося імпортувати всі файли. + Це перезапише ваші поточні налаштування. Кіоски Набуває популярності - Тловий програвач - Віконний програвач - Усунути - Утримуйте, для додавання до черги - Додати до черги тлового відтворення - Додати до черги нового віконного відтворення - Розпочати відтворення звідси - Розпочати звідси у тловому програвачеві - Розпочати відтворення у вікні звідси - Відкрити шухляду - Закрити шухляду - Незабаром тут щось буде ;D + Програвач у тлі + Програвач у вікні + Видалити + Утримуйте, щоб додати до черги + Додати до черги при програванні у тлі + Додати до черги при програванні у вікні + Розпочати програвання звідси + Розпочати звідси при програванні у тлі + Розпочати звідси при програванні у вікні + Відкрити бічну панель + Закрити бічну панель + Незабаром тут щось з\'явиться ;D Завжди питати - Новий плейлист + Новий список відтворення Видалити Змінити назву Назва - Додати до плейлиста - Установити як ескіз плейлиста - Додати плейлист до закладок - Усунути закладку - Чи видалити цього плейлиста\? - Плейлист було створено - Додано до плейлиста - Ескіз плейлиста змінився. - Неможливо видалити плейлист. - Субтитри відсутні - Замостити + Додати до списку відтворення + Призначити ескізом списку відтворення + Додати список відтворення до закладок + Видалити закладку + Видалити цей список відтворення\? + Список відтворення створено + Додано до списку відтворення + Ескіз списку відтворення змінено. + Не вдалося видалити список відтворення. + Без субтитрів + Вписати Заповнити Збільшити - Створено автоматичним шляхом + Створені автоматично Увімкнути LeakCanary - Моніторування витіків пам\'яті може призвести до нереагівності застосунку + Моніторинг втрат пам\'яті може призвести до нереагування додатку під час запису дампу Зазвітувати out-of-lifecycle хиби Примусове звітування про неможливість доставлення Rx винятків, які відбуваються за межами фраґменту або діяльності життєвого циклу після усунення - Використовувати неточний пошук + Використовувати швидкий неточний пошук Неточний пошук дозволяє програвачеві рухатися позиціями швидше, проте з меншою точністю - Додавати в чергу наступний стрим - Автоматично додавати пов\'язаний стрим під час відтворення останнього у черзі без повторювань. + Автоматично додавати в чергу наступний запис + Автоматично додавати пов\'язаний запис під час відтворення останнього у черзі без повторювань. Файл Такої теки не існує - Не існує файлу/джерела контенту - Файл не існує, або немає дозволу на його запис або читання - Назва файлу не може бути порожньою - Помилка: %1$s + Такого джерела файлу/контенту не існує + Файл не існує або відсутній дозвіл на його читання або запис + Ім\'я файлу не може бути порожнім + Сталася помилка: %1$s Імпортування/експортування Імпортування Імпортувати з @@ -357,36 +357,36 @@ Майте на увазі: ця операція може потребувати багато трафіку. \n \nПродовжуватимете? - Завантажити ескізи - Вимкніть завантаженні ескізів, заощаджується трафік та внутрішня пам\'ять. Зміни призведуть очищення кешу зображень. + Завантажувати ескізи + Вимкніть для запобігання завантаженню ескізів, що заощадить трафік та внутрішню пам\'ять. Зміни призведуть до очищення кешу зображень. Кеш зображень стерто Стерти кеш метаданих - "Усунути всі кешовані дані вебсторінки " + Видалити всі кешовані дані веб-сторінок Кеш метаданих стерто Реґулятори швидкісті відтворення Темп Тон Від\'єднати (може спричинити спотворення) - Стрими недоступні для завантаження - Переважний спосіб \"відкриття\" + Відсутні потоки, доступні для завантаження + Бажана дія при відкритті Типова дія під час відкриття вмісту — %s Субтитри - Змінення маштабу субтитрового тексту та тлових стилів. Увімкнення функції потребує перезавантаження застосунку. - Не знайдено відповідного застосунку для відтворення цього файлу + Зміна висоти тексту субтитрів та стилів тла. Потребує перезапуску додатку. + Не встановлено додаток для програвання цього файлу Очистити історію переглядів Видаляє історію відтворень - Чи видалити всю історію переглядів\? - Історію переглядів було видалено. - Очистити історію пошуків + Видалити всю історію переглядів\? + Історію переглядів видалено. + Очистити історію пошуку Видаляє історію пошукових ключових слів - Чи видалити всю пошукову історію\? - Пошукову історію видалено. - Видалено один фрагмент. - "NewPipe є вільною копілефт програмою: Ви можете використовувати її, поширювати та вдосконалювати з розсудом. Зокрема ви можете перерозподіляти та/або змінювати її за умов використання GNU General Public License, опублікованою Free Software Foundation, під 3-ю версією ліцензії, або (на ваш вибір) будь-якою пізнішою версією." - Бажаєте імпортувати з налаштуваннями\? - NewPipe\'ова політика приватності - Проєкт NewPipe дуже серйозно ставиться до вашої приватності. Тому застосунок не збирає ніяких даних без вашої згоди -\nПолітика приватності NewPipe пояснює у деталях, які дані було відіслано та збережено, під час відсилання крахового звіту. + Видалити всю історію пошуку\? + Історію пошуку видалено. + Видалено 1 елемент. + NewPipe є вільним копілефт додатком: ви можете використовувати, поширювати та вдосконалювати його на власний розсуд. Зокрема, ви можете розповсюджувати та/або змінювати її за умов дотримання вимог GNU General Public License 3-ї версії або (на ваш вибір) будь-якої пізнішої версії, опублікованої Free Software Foundation. + Імпортувати разом з налаштуваннями\? + Політика приватності NewPipe + Проект NewPipe дуже серйозно ставиться до вашої приватності. Тому додаток не збирає ніяких даних без вашої згоди. +\nПолітика приватності NewPipe пояснює у деталях, які дані відсилаються та зберігаються, коли ви надсилаєте звіт про падіння додатку. Читати політику приватності Аби дотриматися Норм загального захисту даних ЄС (General Data Protection Regulation, GDPR), ми просимо звернути вашу увагу на NewPipe\'ову політику приватності. Будь ласка, прочитайте уважно \nВи маєте підтвердити її аби надіслати нам баґового звіту. @@ -403,8 +403,8 @@ До тлового програвача Зменшити до віконного програвачу Канали - Плейлисти - Стежки + Списки відтворення + Треки Користувачі Вигляд списку Список @@ -413,18 +413,18 @@ Нова вкладка Обрати вкладку Жест керування гучністю - Змінювати гучність плеєра жестами - Жест регулювання яскравістю - Змінювати яскравість жестами + Змінювати гучність звуку жестами + Жест керування яскравістю + Змінювати яскравість екрану жестами Оновлення Події Файл видалено - Сповіщення про оновлення + Сповіщення про оновлення додатку Сповіщення про нову версію NewPipe Зовнішнє сховище недоступне - Відновити на типові - Бажаєте відновити на типові\? - Кількість підписників недоступне + Відновити типові налаштування + Бажаєте відновити типові налаштування\? + Кількість підписників недоступна Вибір Конференції Оновлення @@ -463,4 +463,11 @@ Максимум спроб Максимальна кількість спроб перед скасуванням завантаження Призупиняти завантаження при переході на стільникові дані + Завантаження до зовнішньої SD-карти поки що неможливе. Скинути розташування теки для завантажень\? + Помилка зчитування збережених вкладок. Використовую типові вкладки. + Вкладки, що відображаються на головній сторінці + Показувати сповіщення з пропозицією оновити додаток за наявності нової версії + Запитуваний діапазон неприпустимий + Продовжити ваші %s відкладених переміщень із Завантажень + Завантаження, що не можуть бути призупинені, будуть перезапущені \ No newline at end of file From c37b88a23929a2a407497fb85f7ceb1461d53817 Mon Sep 17 00:00:00 2001 From: Stjepan Date: Thu, 4 Apr 2019 12:13:45 +0000 Subject: [PATCH 051/138] Translated using Weblate (Croatian) Currently translated at 99.3% (440 of 443 strings) --- app/src/main/res/values-hr/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 9a9bbe004..6e8595010 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -36,7 +36,7 @@ Mapa za preuzimanje zvuka Preuzeti zvuk je spremljen ovdje Unesi put za preuzimanje zvučne datoteke - Auto. reprod. kada je NewPipe pozvan iz druge apl. + Automatska reprodukcija Reproducira videozapis kad je NewPipe pozvan iz druge aplikacije Zadana razlučivost Zadana razlučivost skočnog prozora From 82cb71bf3f15d6457a964239138bc42471b119d0 Mon Sep 17 00:00:00 2001 From: Florian Date: Fri, 5 Apr 2019 13:34:21 +0000 Subject: [PATCH 052/138] Translated using Weblate (French) Currently translated at 99.8% (442 of 443 strings) --- app/src/main/res/values-fr/strings.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index bc63d927f..6d8d03dec 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -471,4 +471,7 @@ Échec du post-traitement Nombre maximum de tentatives Nombre maximum de tentatives avant d\'annuler le téléchargement + Utilisation des onglets par défaut, erreur lors de la lecture des onglets enregistrés + Le serveur n\'accepte pas les téléchargements multi-threads, réessayez avec @string/msg_threads = 1 + Continuer vos %s transferts en attente depuis Téléchargement \ No newline at end of file From 704b8f61dd780d10611c1c5d144917e25b1a2fea Mon Sep 17 00:00:00 2001 From: artik banana Date: Sat, 6 Apr 2019 18:42:13 +0000 Subject: [PATCH 053/138] Translated using Weblate (Hebrew) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-he/strings.xml | 132 +++++++------------------ 1 file changed, 36 insertions(+), 96 deletions(-) diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 5782a1b52..8c23a815c 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -1,6 +1,6 @@ - יש ללחוץ על „חיפוש” כדי להתחיל + לחץ על „חיפוש” כדי להתחיל %1$s צפיות פורסם ב־%1$s לא נמצאו נגני צפייה ישירה, להתקין את VLC\? @@ -22,15 +22,12 @@ מצב חלון צף של NewPipe רקע חלון צף - נתיב להורדת סרטונים נתיב מיקום לאחסון סרטונים נא להקליד נתיב לשמירת סרטונים - תיקיית הורדות שמע הורדות שמע נשמרות כאן נא להקליד נתיב לשמירת קובצי שמע - ניגון אוטומטי מנגן סרטון כאשר NewPipe נפתח דרך יישומון אחר רזולוציית בררת המחדל @@ -54,7 +51,6 @@ שימוש במחוות כדי לשלוט בבהירות ובעצמת השמע של הנגן הצעות חיפוש הצגת הצעות בעת החיפוש - הורדה הבא להציג סרטונים דומים ובאים בתור @@ -69,7 +65,7 @@ נגינה תוכן תוכן עם הגבלת גיל - הצגת סרטונים עם הגבלת גיל. ניתן לאפשר תכנים שכאלה דרך ההגדרות. + הצגת סרטונים עם הגבלת גיל. ניתן לאפשר תכנים שכאלו דרך ההגדרות. חי הורדות הורדות @@ -83,7 +79,6 @@ רענון פינוי שינוי גודל - שגיאה שגיאת רשת אין אפשרות לטעון את כל התמונות הממוזערות @@ -104,17 +99,14 @@ מידע: מה קרה: מה:\\nבקשה:\\nשפת התוכן:\\nשירות:\\nשעון גריניץ׳:\\nחבילה:\\nגרסה:\\nגרסת מערכת ההפעלה: -עריכת מינוי - נרשמת + הירשם + רשום ביטול מינוי לערוץ לא הצלחתי לשנות מינוי לא ניתן לעדכן את המינוי - ראשי מינויים - מה חדש - היסטוריית חיפוש שמירת שאילתות החיפוש מקומית היסטוריה ומטמון @@ -132,20 +124,14 @@ רזולוציה מיטבית ביטול לנגן הכול - התראה מ־NewPipe התראות עבור נגן הרקע והנגן הצף של NewPipe - [לא ידוע] - נגינת התזרים לא הצליחה אירעה תקלה בנגן ממנה לא ניתן להשתקם מתבצעת החלמה משגיאת נגן - ההערה שלך (באנגלית): פרטים: - - תמונה ממוזערת לתצוגה המקדימה של הסרטון תמונות ממוזערות לתצוגה המקדימה של הסרטון תמונה ייצוגית של המפרסם @@ -157,54 +143,45 @@ דוח משתמש אין תוצאות אין כאן כלום מלבד צרצרים - לא ניתן ליצור את תיקיית ההורדות ‚%1$s’ תיקיית ההורדות ‚%1$s’ נוצרה - סרטון שמע ניסיון חוזר הגישה לאחסון נדחתה השתמש בנגן הישן השתמש בנגן המובנה Mediaframework - K M B - אין מנויים - מנוי אחד - שני מנויים - %s מנויים - %s מנויים - - + מנוי אחד + שני מנויים + %s מנויים + %s מנויים + אין צפיות - צפייה אחת - שתי צפיות - %s צפיות - %s צפיות - - + צפייה אחת + שתי צפיות + %s צפיות + %s צפיות + אין סרטונים - סרטון - שני סרטונים - %s סרטונים - %s סרטונים - - + סרטון + שני סרטונים + %s סרטונים + %s סרטונים + התחלה השהיה נגינה מחיקה גיבוב לאימות - משימה חדשה אישור - שם קובץ תת־דיונים שגיאה @@ -218,19 +195,15 @@ נא לבחור תיקיית הורדה זמינה הרשאה זו נדרשת לטובת \nפתיחה בחלון צף - reCAPTCHA אתגר reCAPTCHA התקבלה בקשה לאתגר reCAPTCHA - הורדה רשימת תווים אפשרית בשמות קבצים תווים לא נתמכים מוחלפים בערך הזה תו חלופי - אותיות וספרות רוב התווים המיוחדים - על אודות NewPipe הגדרות על אודות @@ -252,8 +225,6 @@ מומלץ לבקר באתר של NewPipe לפרטים נוספים ולחדשות. הרישיון של NewPipe הצגת הרישיון - - היסטוריה נשלח כחיפוש נצפו @@ -263,17 +234,15 @@ ההיסטוריה התרוקנה פריט נמחק למחוק את הפריט הזה מהיסטוריית החיפושים\? - תוכן הדף הראשי דף ריק דף גישה מזדמנת - דף מינויים + דף מנוים דף עדכונים דף ערוצים נא לבחור ערוץ אין עדיין מינויים לערוצים נא לבחור סוג גישה מזדמנת - גישה מזדמנת החמים 50 המובילים @@ -289,31 +258,26 @@ להתחיל לנגן מכאן "להתחיל מכאן כאשר נגן הרקע מופעל" להתחיל כאן בנגן הצף -הורדת קובץ הזרמה + הורדת קובץ הזרמה הצגת מידע - רשימות נגינה מסומנות - - הוספה אל - - מדינת תוכן כבררת מחדל + הוסף ל + מדינת תוכן ברירת המחדל שירות ניפוי שגיאות תמיד חד פעמי - ייבוא מסד נתונים ייצוא מסד נתונים נגנים חיצוניים לא תומכים בסוגי קישורים כאלה כתובת שגויה קובץ - העברה לרקע העברה לחלון צף לא נמצא נגן צפייה ישירה (ניתן להתקין את VLC כדי לתקן זאת). - מטמון התמונות התרוקן - ניקוי מטמון נתוני העל - מטמון נתוני העל התרוקן + תמונות מטמון נמחקו + מחק מטא-נתונים במטמון + מטא-נתונים במטמון נמחקו ייצוא היסטוריה, מינויים ורשימות נגינה מחיקת היסטוריית הצפייה היסטוריית הצפייה נמחקה. @@ -322,30 +286,24 @@ הקובץ אינו קיים או שחסרה הרשאה לקרוא אותו או לכתוב אליו שם הקובץ אינו יכול להיות ריק ניתן לסדר מחדש בגרירה - יצירה למחוק אחד למחוק הכול התעלמות שינוי שם - פריט אחד נמחק. - לא מותקן יישומון שמתאים לנגינת הקובץ הזה - - למחוק את הפריט הזה מהיסטוריית הצפייה שלך\? + האם ברצונך למחוק פריט זה מהיסטוריית הצפייה\? למחוק את כל הפריטים מההיסטוריה\? הייצוא הסתיים הייבוא הסתיים אין קובץ ZIP תקין - נגן וידאו + נגן סרטונים נגן רקע נגן צף לשאול תמיד - - המידע מתקבל… + משיג מידע… התוכן המבוקש בטעינה - רשימת נגינה חדשה מחיקה שינוי שם @@ -353,30 +311,23 @@ הוספה לרשימת נגינה הוספה רשימת השמעה לסימניות הסרת סימנייה - למחוק רשימת נגינה זו\? רשימת הנגינה נוצרה נוסף לרשימת הנגינה תמונת רשימת הנגינה הוחלפה. מילוי תקריב - נוצרו אוטומטית - ייבוא/ייצוא ייבוא ייבוא מ־ ייצוא אל - - מתבצע ייבוא… - מתבצע ייצוא… - + מייבא… + מייצא… ייבוא קובץ ייצוא קודם - לא ניתן לייבא את המינויים לא ניתן לייצא את המינויים - כדי לייבא את רשימת המינויים שלך מ־YouTube עליך להוריד את קובץ הייצוא: \n \n1. לעבור לכתובת הזו: %1$s @@ -384,7 +335,7 @@ \n3. ההורדה אמורה להתחיל (זה קובץ הייצוא) קצב ברירת מחדל -שימוש בחיפוש מהיר גס + השתמש בחיפוש מהיר, לא מדויק חיפוש גס מאפשר לנגן לחפש נקודת זמן מהר יותר, ברמת דיוק נמוכה יותר טעינת תמונות ממוזערות כיבוי האפשרות מונע את טעינת התמונות הממוזערות, חוסך בתקשורת נתונים ובניצולת הזיכרון. שינויים באפשרות זו מוחקים את המטמון בזיכרון ובכונן. @@ -393,7 +344,6 @@ להוסיף אוטומטית תזרים דומה בעת נגינת התזרים האחרון בתור שאינו מחזורי. החלפת כיווניות העברה לראשי - משכתב את ההיסטוריה והמינויים הנוכחיים שלך מחיקת היסטוריית התזרימים שהתנגנו ערוצים @@ -409,37 +359,27 @@ אין מקור תיקייה/תוכן אירעה שגיאה: %1$s אין תזרימים זמינים להורדה - מדיניות הפרטיות של NewPipe מיזם NewPipe שומר על הפרטיות שלך בצורה מאוד קפדנית. לפיכך, היישומון אינו אוסף נתונים ללא הסכמתך. \nמדיניות הפרטיות של NewPipe מסבירה בפרטי פרטים אילו נתונים נשלחים ומאוחסנים בעת שליחת דיווח על תקלה. הצגת מדיניות הפרטיות NewPipe הוא יישומון חופשי בהתאם לרישיון קופילפט: מותר לך להשתמש, לחקור, לשתף ולשפר בכל דרך שנראית לך. במיוחד מותר לך להפיץ מחדש ו/או לשנות תחת תנאי הרישיון הציבורי הכללי של GNU כפי שמופץ על ידי קרן התכנה החופשית, בין אם גרסה 3 של הרישיון או (לשיקולך) כל גרסה עדכנית יותר שלו. - התנגנו אחרונים + נוגן אחרון הכי נצפים - - אזהרה: ייבוא חלק מהקבצים נכשל. - פעולה זו תדרוס את ההגדרות הקיימות. + אזהרה: לא יכל לייבא את כל הקבצים + פעולה זו תדרוס את ההתקנה הקיימת. לייבא גם הגדרות\? - פתיחת מגירה סגירת מגירה משהו יופיע כאן בקרוב ‎;D - - פעולת ‚פתיחה’ מועדפת פעולת בררת מחדל בעת פתיחת תוכן - %s - - הגדרה כתמונת רשימת הנגינה - + קבע כתמונה לרשימת השמעה לא ניתן למחוק רשימת נגינה. - אין כתוביות - התאמה כתוביות שינוי גודל כותרת הנגן וסגנונות הרקע. נדרשת הפעלה מחדש כדי ששינויים אלה יכנסו לתוקף. - הפעלת LeakCanary מעקב אחר זליגת זיכרון עשויה לגרום ליישומון להיות בלתי זמין בזמן העתקת תוכן הזיכרון לקובץ המזהה שלך, soundcloud.com/המזהה שלך From eb265300fc17b0719009f00b4b9a03bdefb7d42b Mon Sep 17 00:00:00 2001 From: Yehuda Levy Date: Sat, 6 Apr 2019 18:57:47 +0000 Subject: [PATCH 054/138] Translated using Weblate (Hebrew) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-he/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 8c23a815c..419ca8a78 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -419,7 +419,7 @@ בחירה אירועים כנסים - דיווח על שגיאות של חריגה ממחזור חיים + דווח על שגיאות שמחוץ למחזור החיים אילוץ דיווח על חריגות מחוץ למקטעים או למחזור חיי הפעילות לאחר ההשלכה בתשדורת יוצאת ניתן לייבא פרופיל SoundCloud על ידי הקלדת הכתובת או המזהה שלך: \n From 72e30d8e40e49114457ed5fecbf715833338bd6e Mon Sep 17 00:00:00 2001 From: Yaron Shahrabani Date: Sat, 6 Apr 2019 18:59:48 +0000 Subject: [PATCH 055/138] Translated using Weblate (Hebrew) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-he/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 419ca8a78..bad136a23 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -99,7 +99,7 @@ מידע: מה קרה: מה:\\nבקשה:\\nשפת התוכן:\\nשירות:\\nשעון גריניץ׳:\\nחבילה:\\nגרסה:\\nגרסת מערכת ההפעלה: - הירשם + עריכת מינוי רשום ביטול מינוי לערוץ לא הצלחתי לשנות מינוי From f7a534a0d0023ecef9eef685aa6ecb30d5b21ebc Mon Sep 17 00:00:00 2001 From: AB Date: Sat, 6 Apr 2019 07:56:01 +0000 Subject: [PATCH 056/138] Translated using Weblate (Ukrainian) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-uk/strings.xml | 36 +++++++++++++------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 93dbaf48c..6bd68e049 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -23,10 +23,10 @@ Тека для завантаженого аудіо Завантажене аудіо зберігається тут Автоматичне програвання - Програє відео коли NewPipe викликано з іншого додатку + Програє відео коли NewPipe викликано з іншого застосунку Типова роздільна здатність Програти у Kodi - Додаток Kore не знайдено. Встановити його\? + Застосунок Kore не знайдено. Встановити його\? Показати опцію \"Програти у Kodi\" Показати опцію програвання відео у медіацентрі Kodi Аудіо @@ -44,7 +44,7 @@ Інше Програвання у тлі Програти - В доступі до сховища відмовлено + У доступі до сховища відмовлено Контент Контент з віковими обмеженнями Показувати відео з віковими обмеженнями. Дозволити програвання таких записів можна у Налаштуваннях. @@ -77,7 +77,7 @@ Пізніше Вимкнено Не вдалося завантажити зображення - Додаток/інтерфейс впав + Застосунок/інтерфейс впав Ваш коментар (англійською): Подробиці: Ескіз попереднього перегляду відео @@ -100,7 +100,7 @@ Видалити Контрольна сума ОК - Ім\'я файлу + Назва файлу Потоки Помилка Сервер не підтримується @@ -190,7 +190,7 @@ Більшість спеціальних символів Про NewPipe Налаштування - Про додаток + Про застосунок Не вдалося завантажити ліцензію Ліцензії Посприяти @@ -241,7 +241,7 @@ Перейменувати Ліцензії третіх сторін Відкрити веб-сайт - Про додаток + Про застосунок Вільне та легке потокове програвання на Android. Які б не були Ваші ідеї: переклад, дизайн, легкий чи глобальний рефакторинг - будь-яка допомога завжди у нагоді. Що більше зроблено, то ліпшим стає NewPipe! Налагодження @@ -257,7 +257,7 @@ Запит на перевірку reCAPTCHA © %1$s, %2$s під %3$s Учасники - NewPipe розроблений добровольцями, які витрачають власний час заради вашого задоволення. Допоможіть розробникам зробити NewPipe ще кращим, насолоджуючись філіжанкою кави. + NewPipe розроблений добровольцями, які витрачають власний час заради вашого задоволення. Допоможіть розробникам зробити NewPipe ще ліпшим, насолоджуючись філіжанкою кави. Ваша допомога Завітайте на офіційний сайт NewPipe, аби отримати більше інформації та новин. Ліцензія NewPipe @@ -319,7 +319,7 @@ Збільшити Створені автоматично Увімкнути LeakCanary - Моніторинг втрат пам\'яті може призвести до нереагування додатку під час запису дампу + Моніторинг втрат пам\'яті може призвести до нереагування застосунку під час запису дампу Зазвітувати out-of-lifecycle хиби Примусове звітування про неможливість доставлення Rx винятків, які відбуваються за межами фраґменту або діяльності життєвого циклу після усунення Використовувати швидкий неточний пошук @@ -330,7 +330,7 @@ Такої теки не існує Такого джерела файлу/контенту не існує Файл не існує або відсутній дозвіл на його читання або запис - Ім\'я файлу не може бути порожнім + Назва файлу не може бути порожнім Сталася помилка: %1$s Імпортування/експортування Імпортування @@ -371,8 +371,8 @@ Бажана дія при відкритті Типова дія під час відкриття вмісту — %s Субтитри - Зміна висоти тексту субтитрів та стилів тла. Потребує перезапуску додатку. - Не встановлено додаток для програвання цього файлу + Зміна висоти тексту субтитрів та стилів тла. Потребує перезапуску застосунку. + Не встановлено застосунків для програвання цього файлу Очистити історію переглядів Видаляє історію відтворень Видалити всю історію переглядів\? @@ -382,11 +382,11 @@ Видалити всю історію пошуку\? Історію пошуку видалено. Видалено 1 елемент. - NewPipe є вільним копілефт додатком: ви можете використовувати, поширювати та вдосконалювати його на власний розсуд. Зокрема, ви можете розповсюджувати та/або змінювати її за умов дотримання вимог GNU General Public License 3-ї версії або (на ваш вибір) будь-якої пізнішої версії, опублікованої Free Software Foundation. + NewPipe є вільним копілефт застосунком: ви можете використовувати, поширювати та вдосконалювати його на власний розсуд. Зокрема, ви можете розповсюджувати та/або змінювати її за умов дотримання вимог GNU General Public License 3-ї версії або (на ваш вибір) будь-якої пізнішої версії, опублікованої Free Software Foundation. Імпортувати разом з налаштуваннями\? Політика приватності NewPipe - Проект NewPipe дуже серйозно ставиться до вашої приватності. Тому додаток не збирає ніяких даних без вашої згоди. -\nПолітика приватності NewPipe пояснює у деталях, які дані відсилаються та зберігаються, коли ви надсилаєте звіт про падіння додатку. + Проект NewPipe дуже серйозно ставиться до вашої приватності. Тому застосунок не збирає ніяких даних без вашої згоди. +\nПолітика приватності NewPipe пояснює у деталях, які дані відсилаються та зберігаються, коли ви надсилаєте звіт про падіння застосунку. Читати політику приватності Аби дотриматися Норм загального захисту даних ЄС (General Data Protection Regulation, GDPR), ми просимо звернути вашу увагу на NewPipe\'ову політику приватності. Будь ласка, прочитайте уважно \nВи маєте підтвердити її аби надіслати нам баґового звіту. @@ -419,7 +419,7 @@ Оновлення Події Файл видалено - Сповіщення про оновлення додатку + Сповіщення про оновлення застосунку Сповіщення про нову версію NewPipe Зовнішнє сховище недоступне Відновити типові налаштування @@ -433,7 +433,7 @@ Доступна нова версія NewPipe! Натисніть для завантаження Завершено - В черзі + У черзі призупинено додано в чергу пост-обробка @@ -466,7 +466,7 @@ Завантаження до зовнішньої SD-карти поки що неможливе. Скинути розташування теки для завантажень\? Помилка зчитування збережених вкладок. Використовую типові вкладки. Вкладки, що відображаються на головній сторінці - Показувати сповіщення з пропозицією оновити додаток за наявності нової версії + Показувати сповіщення з пропозицією оновити застосунок за наявності нової версії Запитуваний діапазон неприпустимий Продовжити ваші %s відкладених переміщень із Завантажень Завантаження, що не можуть бути призупинені, будуть перезапущені From 943e03f9d84f71800b61c999995a3a5838425038 Mon Sep 17 00:00:00 2001 From: Yaron Shahrabani Date: Sat, 6 Apr 2019 19:00:32 +0000 Subject: [PATCH 057/138] Translated using Weblate (Hebrew) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-he/strings.xml | 30 +++++++++++++------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index bad136a23..88b818190 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -1,6 +1,6 @@ - לחץ על „חיפוש” כדי להתחיל + יש ללחוץ על „חיפוש” כדי להתחיל %1$s צפיות פורסם ב־%1$s לא נמצאו נגני צפייה ישירה, להתקין את VLC\? @@ -100,7 +100,7 @@ מה קרה: מה:\\nבקשה:\\nשפת התוכן:\\nשירות:\\nשעון גריניץ׳:\\nחבילה:\\nגרסה:\\nגרסת מערכת ההפעלה: עריכת מינוי - רשום + נרשמת ביטול מינוי לערוץ לא הצלחתי לשנות מינוי לא ניתן לעדכן את המינוי @@ -237,7 +237,7 @@ תוכן הדף הראשי דף ריק דף גישה מזדמנת - דף מנוים + דף מינויים דף עדכונים דף ערוצים נא לבחור ערוץ @@ -261,8 +261,8 @@ הורדת קובץ הזרמה הצגת מידע רשימות נגינה מסומנות - הוסף ל - מדינת תוכן ברירת המחדל + הוספה אל + מדינת תוכן כבררת מחדל שירות ניפוי שגיאות תמיד @@ -276,8 +276,8 @@ העברה לחלון צף לא נמצא נגן צפייה ישירה (ניתן להתקין את VLC כדי לתקן זאת). תמונות מטמון נמחקו - מחק מטא-נתונים במטמון - מטא-נתונים במטמון נמחקו + ניקוי מטמון נתוני העל + מטמון נתוני העל התרוקן ייצוא היסטוריה, מינויים ורשימות נגינה מחיקת היסטוריית הצפייה היסטוריית הצפייה נמחקה. @@ -293,7 +293,7 @@ שינוי שם פריט אחד נמחק. לא מותקן יישומון שמתאים לנגינת הקובץ הזה - האם ברצונך למחוק פריט זה מהיסטוריית הצפייה\? + למחוק את הפריט הזה מהיסטוריית הצפייה שלך\? למחוק את כל הפריטים מההיסטוריה\? הייצוא הסתיים הייבוא הסתיים @@ -302,7 +302,7 @@ נגן רקע נגן צף לשאול תמיד - משיג מידע… + המידע מתקבל… התוכן המבוקש בטעינה רשימת נגינה חדשה מחיקה @@ -322,8 +322,8 @@ ייבוא ייבוא מ־ ייצוא אל - מייבא… - מייצא… + מתבצע ייבוא… + מתבצע ייצוא… ייבוא קובץ ייצוא קודם לא ניתן לייבא את המינויים @@ -335,7 +335,7 @@ \n3. ההורדה אמורה להתחיל (זה קובץ הייצוא) קצב ברירת מחדל - השתמש בחיפוש מהיר, לא מדויק + שימוש בחיפוש מהיר ולא מדויק חיפוש גס מאפשר לנגן לחפש נקודת זמן מהר יותר, ברמת דיוק נמוכה יותר טעינת תמונות ממוזערות כיבוי האפשרות מונע את טעינת התמונות הממוזערות, חוסך בתקשורת נתונים ובניצולת הזיכרון. שינויים באפשרות זו מוחקים את המטמון בזיכרון ובכונן. @@ -367,14 +367,14 @@ נוגן אחרון הכי נצפים אזהרה: לא יכל לייבא את כל הקבצים - פעולה זו תדרוס את ההתקנה הקיימת. + פעולה זו תדרוס את ההגדרות הקיימות. לייבא גם הגדרות\? פתיחת מגירה סגירת מגירה משהו יופיע כאן בקרוב ‎;D פעולת ‚פתיחה’ מועדפת פעולת בררת מחדל בעת פתיחת תוכן - %s - קבע כתמונה לרשימת השמעה + הגדרה כתמונת רשימת הנגינה לא ניתן למחוק רשימת נגינה. אין כתוביות התאמה @@ -419,7 +419,7 @@ בחירה אירועים כנסים - דווח על שגיאות שמחוץ למחזור החיים + דיווח על שגיאות שמחוץ למחזור החיים אילוץ דיווח על חריגות מחוץ למקטעים או למחזור חיי הפעילות לאחר ההשלכה בתשדורת יוצאת ניתן לייבא פרופיל SoundCloud על ידי הקלדת הכתובת או המזהה שלך: \n From 3ff47623d5b421a715f3f0b121deb90da276bd06 Mon Sep 17 00:00:00 2001 From: artik banana Date: Sat, 6 Apr 2019 19:00:50 +0000 Subject: [PATCH 058/138] Translated using Weblate (Hebrew) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-he/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 88b818190..d6a59eab4 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -366,7 +366,7 @@ NewPipe הוא יישומון חופשי בהתאם לרישיון קופילפט: מותר לך להשתמש, לחקור, לשתף ולשפר בכל דרך שנראית לך. במיוחד מותר לך להפיץ מחדש ו/או לשנות תחת תנאי הרישיון הציבורי הכללי של GNU כפי שמופץ על ידי קרן התכנה החופשית, בין אם גרסה 3 של הרישיון או (לשיקולך) כל גרסה עדכנית יותר שלו. נוגן אחרון הכי נצפים - אזהרה: לא יכל לייבא את כל הקבצים + אזהרה: לא יכל לייבא את כל הקבצים. פעולה זו תדרוס את ההגדרות הקיימות. לייבא גם הגדרות\? פתיחת מגירה From 115b44585ba8c1485dc6521e47787d8fbc03ad6b Mon Sep 17 00:00:00 2001 From: Yaron Shahrabani Date: Sat, 6 Apr 2019 19:10:03 +0000 Subject: [PATCH 059/138] Translated using Weblate (Hebrew) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-he/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index d6a59eab4..e0eae8041 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -366,7 +366,7 @@ NewPipe הוא יישומון חופשי בהתאם לרישיון קופילפט: מותר לך להשתמש, לחקור, לשתף ולשפר בכל דרך שנראית לך. במיוחד מותר לך להפיץ מחדש ו/או לשנות תחת תנאי הרישיון הציבורי הכללי של GNU כפי שמופץ על ידי קרן התכנה החופשית, בין אם גרסה 3 של הרישיון או (לשיקולך) כל גרסה עדכנית יותר שלו. נוגן אחרון הכי נצפים - אזהרה: לא יכל לייבא את כל הקבצים. + אזהרה: ייבוא חלק מהקבצים נכשל. פעולה זו תדרוס את ההגדרות הקיימות. לייבא גם הגדרות\? פתיחת מגירה From f86d755890b399bbc7955b1c6db00b87b973fcc3 Mon Sep 17 00:00:00 2001 From: Yaron Shahrabani Date: Sun, 7 Apr 2019 13:59:17 +0000 Subject: [PATCH 060/138] Translated using Weblate (Hebrew) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-he/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index e0eae8041..155c6a223 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -353,8 +353,8 @@ למחוק את כל היסטוריית הצפייה\? מחיקת היסטוריית מילות החיפוש למחוק את כל היסטוריית החיפוש\? - לא נמצא תזרימי וידאו - לא נמצא תזרימי שמע + לא נמצאו תזרימי וידאו + לא נמצאו תזרימי שמע אין תיקייה כזו אין מקור תיקייה/תוכן אירעה שגיאה: %1$s From 6cdea85a493e6a1ea248a7cbf432246c85c4b9d5 Mon Sep 17 00:00:00 2001 From: Yoav Date: Sun, 7 Apr 2019 13:59:45 +0000 Subject: [PATCH 061/138] Translated using Weblate (Hebrew) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-he/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 155c6a223..5738254cb 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -357,7 +357,7 @@ לא נמצאו תזרימי שמע אין תיקייה כזו אין מקור תיקייה/תוכן - אירעה שגיאה: %1$s + נתקלנו בבעיה: %1$s אין תזרימים זמינים להורדה מדיניות הפרטיות של NewPipe מיזם NewPipe שומר על הפרטיות שלך בצורה מאוד קפדנית. לפיכך, היישומון אינו אוסף נתונים ללא הסכמתך. From 037632fbf06c005217b2ff6df5476d777d34c9eb Mon Sep 17 00:00:00 2001 From: Yaron Shahrabani Date: Sun, 7 Apr 2019 13:59:50 +0000 Subject: [PATCH 062/138] Translated using Weblate (Hebrew) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-he/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 5738254cb..155c6a223 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -357,7 +357,7 @@ לא נמצאו תזרימי שמע אין תיקייה כזו אין מקור תיקייה/תוכן - נתקלנו בבעיה: %1$s + אירעה שגיאה: %1$s אין תזרימים זמינים להורדה מדיניות הפרטיות של NewPipe מיזם NewPipe שומר על הפרטיות שלך בצורה מאוד קפדנית. לפיכך, היישומון אינו אוסף נתונים ללא הסכמתך. From e9444e058c03e06c4cc486a3daee4bee2bb80944 Mon Sep 17 00:00:00 2001 From: minsk21 Date: Tue, 9 Apr 2019 01:07:06 +0000 Subject: [PATCH 063/138] Translated using Weblate (Belarusian) Currently translated at 74.3% (329 of 443 strings) --- app/src/main/res/values-be/strings.xml | 83 +++++--------------------- 1 file changed, 15 insertions(+), 68 deletions(-) diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index 40c277884..8eface954 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -1,5 +1,6 @@ -Націсніце пошук, каб пачаць + + Націсніце пошук, каб пачаць %1$s праглядаў Апублікавана %1$s Патокавы плэер не знойдзены. Усталяваць VLC? @@ -27,25 +28,19 @@ Не атрымалася змяніць падпіску Не атрымалася абнавіць падпіску Паказаць звесткі - Галоўная Падпіскі Адзначаныя плэйлісты - Што новага - У фоне У акне Дадаць да - Шлях загрузкі відэа Папка для захоўвання загружаных відэа Увядзіце шлях да папкі для загрузкі відэа - Тэчка загрузкі аўдыё "Сюды захоўваецца загружанае аўдыё " Увядзіце шлях да папкі для загрузкі аўдыё - Аўтапрайграванне Прайграваць відэа пры выкліку NewPipe з іншага прыкладання Разрознянне па змаўчанні @@ -136,17 +131,13 @@ Заўсёды Толькі цяпер Файл - Апавяшчэнне NewPipe Апавяшчэнні для NewPipe ў фоне і ва ўсплываючым акне - [Невядома] - Пераключыць арыентацыю Перайсці ў фон Перайсці ў акно Перайсці ў галоўнае акно - Імпарт дадзеных Экспарт дадзеных Ваша бягучая гісторыя і падпіскі перазапішуцца @@ -185,7 +176,6 @@ Імя файла не можа быць пустым Адбылася памылка: %1$s Няма патокаў, даступных для загрузкі - Прабачце, гэта не павінна было адбыцца. Адправіць справаздачу па e-mail Прабачце, адбыліся памылкі. @@ -195,8 +185,6 @@ Што:\\nЗапыт:\\nМова кантэнту:\\nСэрвіс:\\nЧас па Грынвічы:\\nПакет:\\nВерсія:\\nВерсія АС: Ваш каментар (English): Падрабязнасці: - - Мініяцюра відэа-прэв\'ю Мініяцюра відэа-прэв\'ю Мініяцюра аватара карыстальніка @@ -209,42 +197,35 @@ Няма вынікаў Нічога няма Перацягніце, каб змяніць парадак - Не атрымалася стварыць папку для загрузкі \"%1$s\" Створана папка для загрузак \"%1$s\" - Відэа Аўдыё Паспрабаваць зноў Няма доступу да носьбіта Стары плэер Стары убудаваны плэер на Mediaframework - тыс. млн. млрд. - Няма падпісчыкаў - %s падпісчык - %s падпісчыка - %s падпісчыкаў - - + %s падпісчык + %s падпісчыка + %s падпісчыкаў + Няма праглядаў - %s прагляд - %s прагляда - %s праглядаў - - + %s прагляд + %s прагляда + %s праглядаў + Няма відэа - %s відэа - %s відэа - %s відэа - - + %s відэа + %s відэа + %s відэа + Пачаць Паўза Прайграць @@ -255,10 +236,8 @@ Кантрольная сума Адхіліць Перайменаваць - Новая мэта ОК - Імя файла Патокі Памылка @@ -273,21 +252,16 @@ Гэтае разрозненне трэба для \nпрайгравання ў акне 1 элемент выдалены. - reCAPTCHA Запыт reCAPTCHA Запытаны ўвод reCAPTCHA - Загрузкі Дапушчальныя сімвалы назвы файлаў Недапушчальныя сімвалы замяняюцца на гэтыя Сімвал для замены - Літары і лічбы Большасць спецзнакаў - Прыкладанне для прайгравання гэтага файла не ўстаноўлена - Аб NewPipe Налады Аб дадатку @@ -314,8 +288,6 @@ Ліцэнзія NewPipe NewPipe - свабоднае праграмнае забеспячэнне: вы можаце выкарыстоўваць, вывучаць і паляпшаць яго па сваім меркаванні. У прыватнасці, вы можаце распаўсюджваць і / або змяняць яго ў адпаведнасці з умовамі GNU General Public License, апублікаванай Free Software Foundation, альбо версіі 3, альбо (па вашаму выбару) любой больш позняй версіі. Прачытаць ліцэнзію - - Гісторыя Гісторыя пошуку Прагледжана @@ -329,7 +301,6 @@ Выдаліць усе элементы з гісторыі? Нядаўна прайграныя Часта прайграваемыя - Кантэнт галоўнай старонкі Пустая старонка Старонка кіёска @@ -345,7 +316,6 @@ Увага: не ўсе файлы былі імпартаваныя. Бягучыя дадзеныя будуць замененыя. Хочаце імпартаваць налады? - Кіёск Трэнды Топ 50 @@ -361,70 +331,51 @@ Відэаплэер Фонавы плэер Плэер у акне - Адкрыць бакавую панэль Зачыніць бакавую панэль Хутка тут сёе-тое з\'явіцца ;D - - Пры адкрыцці кантэнту Пры адкрыцці спасылкі на кантэнт — %s - Відэаплэер Фонавы плэер Плэер у акне Заўсёды пытацца - Атрыманне звестак… Загрузка запытанага кантэнту - Стварыць плэйліст Выдаліць плэйліст Перайменаваць плэйліст Імя Дадаць у плэйліст На мініяцюру плэйліста - Дадаць плэйліст у закладкі Выдаліць закладку - Выдаліць гэты плэйліст? Плэйліст створаны Дададзена ў плэйліст Мініяцюра плэйліста зменена Не атрымалася выдаліць плэйліст - Без тытраў - Падагнаць Запоўніць Наблізіць - Створаны аўтаматычна - Тытры Змяніць памер і фон тытраў. Змены ўступяць у сілу пасля перазапуску - Уключыць LeakCanary Маніторынг уцечкі памяці можа прывесці да завісання прыкладання - Паведамляць пра памылкі жыццёвага цыклу Прымусова паведамляць пра недастаўляемыя Rx-выключэнні па-за фрагментам або жыццёвым цыкле пасля выдалення - Імпарт/Экспарт Імпарт Імпарт з Экспарт у - Імпарт… Экспарт… - Імпарт файла Папярэдні экспарт - Не атрымалася імпартаваць падпіскі Не атрымалася экспартаваць падпіскі - Імпарт падпісак з YouTube загрузкай файла экспарту: \n \n1. Перайдзіце на: %1$s @@ -437,11 +388,9 @@ \n3. Увайдзіце, калі неабходна \n4. Скапіруйце адрас з адраснага радка. вашID, soundcloud.com/вашID - Гэтае дзеянне можа выклікаць вялікі расход трафіку. \n \nПрацягнуць? - Кіраванне хуткасцю прайгравання Тэмп Тон @@ -449,12 +398,10 @@ Прапускаць цішыню Крок Скід - У адпаведнасці з Агульным рэгламентам па абароне дадзеных ЕС (GDPR), звяртаем вашу ўвагу на палітыку прыватнасці NewPipe. Калі ласка, уважліва азнаёмцеся з ёй. \nВам неабходна прыняць яе ўмовы, каб адправіць нам справаздачу пра памылку. Прыняць Адмовіцца - Без абмежаванняў Ліміт разрознення ў мабільнай сетцы Пры згортванні плэера @@ -462,5 +409,5 @@ Нічога не рабіць Фонавы плэер Плэер у акне - + Адпісацца \ No newline at end of file From e423192265e080f7116dcc9c506a0f763450871c Mon Sep 17 00:00:00 2001 From: ___ Date: Mon, 8 Apr 2019 08:05:54 +0000 Subject: [PATCH 064/138] Translated using Weblate (Ukrainian) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-uk/strings.xml | 80 +++++++++++++------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 6bd68e049..7ab368400 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -320,8 +320,8 @@ Створені автоматично Увімкнути LeakCanary Моніторинг втрат пам\'яті може призвести до нереагування застосунку під час запису дампу - Зазвітувати out-of-lifecycle хиби - Примусове звітування про неможливість доставлення Rx винятків, які відбуваються за межами фраґменту або діяльності життєвого циклу після усунення + Звітувати про помилки життєвого циклу додатку + Примусове звітування про неможливість доставлення Rx-винятків, які відбуваються за межами фрагменту або діяльності життєвого циклу після усунення Використовувати швидкий неточний пошук Неточний пошук дозволяє програвачеві рухатися позиціями швидше, проте з меншою точністю Автоматично додавати в чергу наступний запис @@ -332,38 +332,38 @@ Файл не існує або відсутній дозвіл на його читання або запис Назва файлу не може бути порожнім Сталася помилка: %1$s - Імпортування/експортування - Імпортування + Імпорт/експорт + Імпорт Імпортувати з Експортувати до Імпортування… Експортування… - Імпортування файлу - Попереднього експортування - Неможливо імпортувати підписання - Неможливо експортувати підписання - Імпортуйте Ютюбові підписання завантаженням файлу еспортування: + Імпортувати файл + Попереднє експортування + Не вдалося імпортувати підписки + Не вдалося експортувати підписки + Імпортуйте підписки YouTube, завантаживши файл експорту: \n -\n1. Перейдіть за цією ланкою: %1$s +\n1. Перейдіть за цією адресою: %1$s \n2. За запитом увійдіть до вашої обліківки -\n3. Завантаження має початися (це й є файл еспортування) - Імпортуйте SoundCloud\'овий профайл уписавши його URL, або ваш ID: -\n -\n1. Увімкніть режимі \"desktop\" у переглядачівеві (сайт не підтримується мобільними ґаджетами) -\n2. Перейдіть за цією ланкою: %1$s -\n3. За запитом увійдіть до вашої обліківки -\n4. Скопіюйте URL профайлу, до якого вас переспрямує. - вашID, soundcloud.com/вашid +\n3. Завантаження має початися (це й є файл еспорту) + Імпортуйте профіль SoundCloud, вписавши або URL, або ваш ID: +\n +\n1. Увімкніть режим \"настільний комп\'ютер\" у веб-браузері (сайт не підтримується мобільними пристроями) +\n2. Перейдіть за цим посиланням: %1$s +\n3. За запитом увійдіть до вашої обліківки +\n4. Скопіюйте URL профілю, до якого ви будете переспрямовані. + ваш ID, soundcloud.com/yourid Майте на увазі: ця операція може потребувати багато трафіку. \n -\nПродовжуватимете? +\nБажаєте продовжити\? Завантажувати ескізи Вимкніть для запобігання завантаженню ескізів, що заощадить трафік та внутрішню пам\'ять. Зміни призведуть до очищення кешу зображень. Кеш зображень стерто Стерти кеш метаданих Видалити всі кешовані дані веб-сторінок Кеш метаданих стерто - Реґулятори швидкісті відтворення + Керування швидкістю відтворення Темп Тон Від\'єднати (може спричинити спотворення) @@ -388,25 +388,25 @@ Проект NewPipe дуже серйозно ставиться до вашої приватності. Тому застосунок не збирає ніяких даних без вашої згоди. \nПолітика приватності NewPipe пояснює у деталях, які дані відсилаються та зберігаються, коли ви надсилаєте звіт про падіння застосунку. Читати політику приватності - Аби дотриматися Норм загального захисту даних ЄС (General Data Protection Regulation, GDPR), ми просимо звернути вашу увагу на NewPipe\'ову політику приватності. Будь ласка, прочитайте уважно -\nВи маєте підтвердити її аби надіслати нам баґового звіту. - Ухвалити + З метою дотримання Загального регламенту про захист даних ЄС (General Data Protection Regulation, GDPR) ми звертаємо вашу увагу на політику приватності NewPipe. Будь ласка, прочитайте уважно. +\nВи маєте прийняти її, аби надіслати нам звіт про помилку. + Прийняти Відхилити - Безмежно - Обмеження якості відео (мобільний трафік) - Перемотувати підчас тиші + Без обмежень + Обмежити роздільну здатність при використанні мобільного трафіку + Пропускати тихі проміжки Крок Скинути Зменшити при перемкненні застосунку - Дія при перемкненні до іншого застосунку з головного відеопрогравача — %s - Жодних - До тлового програвача - Зменшити до віконного програвачу + Дія при перемиканні до іншого застосунку з головного відеопрогравача — %s + Нічого + Зменшити до програвача у тлі + Зменшити до програвача у вікні Канали Списки відтворення Треки Користувачі - Вигляд списку + Режим перегляду списком Список Сiтка Відписатися @@ -441,21 +441,21 @@ Дію заборонено системою Помилка завантаження Завантаження завершено - Завантажень завершено: %s - Створення унікального імені + %s завантажень завершено + Згенерувати унікальне ім\'я Перезаписати - Файл з таким іменем вже існує - Файл з таким іменем вже завантажується + Завантажений файл з таким ім\'ям вже існує + Файл з таким ім\'ям вже завантажується Показати помилку Код - Не можливо створити файл - Не можливо створити теку призначення - Не надано доступу системою + Файл не може бути створений + Цільова тека не може бути створена + Доступ заборонено системою Захищене з\'єднання не встановлено Сервер не знайдено Не вдалося з\'єднатися із сервером - Не вдалося отримати дані з серверу - Сервер не підтримує завантаження на декілька потоків, спробуйте з параметром @string/msg_threads = 1 + Сервер не надсилає дані + Сервер не підтримує завантаження у декілька потоків, спробуйте з параметром @string/msg_threads = 1 Не знайдено Невдала пост-обробка Очистити завершені завантаження From f96a371464f2b559a732f3eb21fa6a818a3e725f Mon Sep 17 00:00:00 2001 From: minsk21 Date: Tue, 9 Apr 2019 01:18:35 +0000 Subject: [PATCH 065/138] Translated using Weblate (Belarusian) Currently translated at 74.5% (330 of 443 strings) --- app/src/main/res/values-be/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index 8eface954..dc9e6b571 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -327,7 +327,7 @@ Налады аўдыё Зацісніце, каб дадаць у чаргу У чаргу \"У фоне\" - У чаргу \"У фоне\" + У чаргу \"У акне\" Відэаплэер Фонавы плэер Плэер у акне From 4751075e879b2bc6aac626b2256244c3a4823aa4 Mon Sep 17 00:00:00 2001 From: Lambda Monad Date: Thu, 11 Apr 2019 18:21:39 +0000 Subject: [PATCH 066/138] Translated using Weblate (Indonesian) Currently translated at 99.3% (440 of 443 strings) --- app/src/main/res/values-id/strings.xml | 99 +++----------------------- 1 file changed, 11 insertions(+), 88 deletions(-) diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index 9e8412e02..66b901ac9 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -14,15 +14,12 @@ Pilih peramban Gunakan pemutar video eksternal Gunakan pemutar audio eksternal - Lokasi unduhan video Lokasi untuk menyimpan video yang diunduh Masukkan lokasi unduhan video - Lokasi unduhan audio Audio yang diunduh disimpan di sini Masukkan lokasi unduhan berkas audio - Putar otomatis Putar video ketika NewPipe dijalankan dari aplikasi lain Resolusi @@ -36,7 +33,6 @@ M4A — kualitas lebih baik Gelap Terang - Unduh Berikutnya Tampilkan video \'Berikutnya\' dan \'Serupa\' @@ -58,7 +54,6 @@ Unduhan Unduhan Laporan galat - Galat Tidak bisa mengurai situs web Tidak dapat menguraikan situs web sepenuhnya @@ -75,8 +70,6 @@ Yang terjadi: Komentar anda (dalam bahasa Inggris): Detail: - - Thumbnail pratinjau video Thumbnail pratinjau video Suka @@ -87,21 +80,17 @@ Laporkan Galat Tidak bisa membuat direktori unduhan \'%1$s\' Direktori unduhan dibuat \'%1$s\' - Video Audio Ulangi Izin akses penyimpanan ditolak - Hapus Putar Mulai Jeda Ceksum - Misi baru OK - Nama berkas Galat Server tidak didukung @@ -112,79 +101,57 @@ Mohon tunggu… Disalin ke papan klip Silakan pilih direktori unduhan yang tersedia - Pemutar stream tidak ditemukan. Apakah anda ingin memasang VLC\? Tidak bisa dekripsi tanda tangan URL video App/UI rusak Tidak bisa mendapatkan stream apapun Apa:\\nPermintaan:\\nBahasa Konten:\\nLayanan:\\nWaktu GMT:\\nPaket:\\nVersi:\\nVersi OS: Laporan pengguna - Thread reCAPTCHA Tantangan reCAPTCHA - Meminta kode reCAPTCHA - Hitam - Semua Channel - R J T - - Ya Nanti - - Buka di mode popup Izin ini dibutuhkan untuk \nmembuka di mode popup - Mode popup NewPipe - Memutar dalam mode popup Gunakan pemutar lama Pemutar Mediaframework bawaan versi lama Dinonaktifkan - Format video - Resolusi popup Tampilkan resolusi yang lebih tinggi Hanya perangkat tertentu yang mendukung pemutaran video 2K/4K Latar Belakang Popup - Segarkan Bersihkan - Filter Menghapus audio pada BEBERAPA resolusi Ingat ukuran dan posisi popup Ingat ukuran dan posisi terakhir popup - Popup Ubah ukuran - Kontrol gestur pemutar Gunakan gestur untuk mengontrol kecerahan dan volume pemutar Saran pencarian Tampilkan saran ketika mencari - -Resolusi terbaik - + Resolusi terbaik Unduh Karakter yang diizinkan sebagai nama berkas Karakter yang tidak valid akan diganti dengan karakter ini Karakter pengganti - Huruf dan angka Karakter spesial umum - Tentang NewPipe Pengaturan Tentang @@ -195,19 +162,17 @@ Tentang Kontributor Lisensi - Aplikasi streaming libre dan ringan untuk Android. + Aplikasi streaming bebas dan ringan untuk Android. Lihat di GitHub Lisensi NewPipe Terlepas apakah anda memiliki ide untuk; terjemahan, perubahan desain, pembersihan kode, atau perubahan kode yang signifikan, segala bantuan akan selalu diterima. Semakin banyak akan semakin baik jadinya! Baca lisensi Kontribusi -Subscribe + Subscribe Disubscribe Apa Yang Baru - Lanjutkan saat fokus Lanjutkan pemutaran setelah interupsi (mis. panggilan telepon) - Utama Riwayat pencarian Simpan pencarian secara lokal @@ -241,16 +206,12 @@ Selalu Hanya Sekali Berkas - Notifikasi untuk pemutar latar belakang dan popup NewPipe - [Tidak diketahui] - Ubah Orientasi Alihkan ke Latar Belakang Alihkan ke Popup Alihkan ke Utama - Impor basis data Ekspor basis data Timpa riwayat dan subscription anda saat ini @@ -267,32 +228,26 @@ Berkas tidak tersedia atau tidak memiliki izin baca atau tulis Nama berkas tidak boleh kosong Telah terjadi galat: %1$s - Tidak ada hasil Tidak ada apapun disini selain jangkrik Geser untuk ubah urutan - Tidak ada subscriber - %s subscriber - - + %s subscriber + Belum ditonton - %s ditonton - - + %s ditonton + Tidak ada video - Video - - + Video + Buat Hapus Satu Hapus Semua Abaikan Ubah nama - Donasi NewPipe dikembangkan oleh relawan yang menyisihkan waktu untuk memberi anda pengalaman terbaik. Segala dukungan kepada pengembang akan membuat NewPipe menjadi lebih baik sambil menikmati secangkir kopi. Beri dukungan @@ -306,7 +261,6 @@ Apakah Anda yakin ingin menghapus semua item dari riwayat\? Terakhir Diputar Sering Diputar - Konten laman utama Laman Kosong Laman Kiosk @@ -321,7 +275,6 @@ Berkas ZIP tidak valid Perhatian: Tidak bisa mengimpor semua berkas. Ini akan menimpa pengaturan anda saat ini. - Kiosk Trending Top 50 @@ -337,58 +290,45 @@ Mulai putar di sini Mulai dari sini ketika di latar belakang Mulai dari sini pada popup baru - Buka Laci Tutup Laci Sesuatu akan segera muncul di sini ;D - - Pemutar video Pemutar latar belakang Pemutar popup Selalu bertanya - Mendapatkan info… Memuat konten yang diminta - Playlist Baru Hapus Ubah Nama Nama Tambahkan Ke Playlist Atur sebagai Thumbnail Playlist - Markah Playlist Hapus Markah - Hapus playlist ini\? Playlist dibuat Diplaylist Thumbnail playlist diubah. Tidak bisa menghapus playlist. - Tidak ada Takarir - Pas Isi Perbesar - Otomatis dibuat Ukuran fon deskripsi Fon lebih kecil Fon normal Fon lebih besar - Aktifkan LeakCanary Nightcore Bawaan -Pemutar stream tidak ditemukan (anda bisa memasang VLC untuk memutarnya). + Pemutar stream tidak ditemukan (anda bisa memasang VLC untuk memutarnya). Unduh berkas stream Tidak bisa mengubah subscription Tampilkan info - Tambahkan Ke - Hapus riwayat tontonan Hapus riwayat stream yang telah diputar Hapus seluruh riwayat tontonan\? @@ -398,14 +338,10 @@ Hapus seluruh riwayat pencarian\? Riwayat pencarian dihapus. Tidak ada stream yang tersedia untuk diunduh - 1 item dihapus. - Tidak ada aplikasi terpasang untuk memutar berkas ini - Ditonton Playlist Disimpan - Antre otomatis stream berikutnya Channel berhenti disubscribe Tidak bisa memperbarui subscription @@ -414,32 +350,23 @@ Memungkinkan pengguna memilih posisi waktu video dengan cepat tetapi dengan tingkat presisi yang rendah NewPipe adalah perangkat lunak libre copyleft: Anda bisa menggunakannya, mempelajarinya, berbagi, dan meningkatkannya. Secara khusus anda bisa mendistribusikan ulang dan/atau memodifikasinya dibawah syarat Lisensi Publik Umum GNU yang diterbitkan oleh Free Software Foundation, baik versi 3 dari Lisensi, atau (sesuai pilihan anda) versi yang lebih baru. Apakah anda juga ingin mengimpor pengaturan\? - Tindakan \'buka\' yang diinginkan Tindakan baku ketika membuka konten — %s - Takarir Ubah skala teks takarir pemutar dan gaya latar belakang. Perlu memulai ulang apl. - Pemantauan kebocoran memori dapat menyebabkan apl menjadi tidak responsif saat heap dumping - Laporkan galat out-of-lifecycle Paksa pelaporan eksepsi Rx yang tak terkirim di luar fragmen atau siklus hidup aktivitas setelah dibuang - Impor/ekspor Impor Impor dari Ekspor ke - Mengimpor… Mengekspor… - Impor berkas Ekspor sebelumnya - Tidak bisa mengimpor subscription Tidak bisa mengekspor subscription - Impor subscription YouTube dengan mengunduh berkas yang diekspor: \n \n1. Kunjungi URL ini: %1$s @@ -452,11 +379,9 @@ \n3. Masuk ketika ditanya \n4. Salin URL profil anda ketika dialihkan. idAnda, soundcloud.com/idAnda - Perlu diingat operasi ini membutuhkan bandwidth yang besar. \n \nApakah anda ingin melanjutkan\? - Kontrol Kecepatan Pemutaran Tempo Nada @@ -472,12 +397,10 @@ Percepat saat diam Langkah Atur ulang - Agar sesuai dengan Regulasi Perlindungan Data Umum Eropa (GDPR), dengan ini kami tarik perhatian anda ke kebijakan privasi NewPipe. Silakan baca dengan seksama. \nAnda harus menerimanya untuk mengirimkan laporan bug kepada kami. Setuju Tolak - Tanpa batas Batasi resolusi saat menggunakan data seluler Minimalkan saat beralih apl @@ -549,5 +472,5 @@ Percobaan maksimum Jumlah upaya maksimum sebelum membatalkan unduhan Berhenti ketika beralih ke data seluler - Unduhan yang tidak dapat dijeda akan dimulai kembali + Unduhan yang tidak dapat dijeda akan diulang dari awal \ No newline at end of file From 4e1423d224296e89551636ec5e02ccc94ef7d421 Mon Sep 17 00:00:00 2001 From: Vasiliy Date: Sat, 13 Apr 2019 10:31:32 +0300 Subject: [PATCH 067/138] Implement playback state management --- .../org/schabi/newpipe/RouterActivity.java | 8 +- .../history/dao/StreamHistoryDAO.java | 5 + .../stream/model/StreamStateEntity.java | 14 + .../fragments/detail/VideoDetailFragment.java | 64 +- .../fragments/list/BaseListFragment.java | 6 +- .../list/channel/ChannelFragment.java | 16 +- .../list/playlist/PlaylistFragment.java | 22 +- .../local/history/HistoryRecordManager.java | 49 +- .../history/StatisticsPlaylistFragment.java | 16 +- .../local/playlist/LocalPlaylistFragment.java | 16 +- .../newpipe/player/BackgroundPlayer.java | 1 + .../org/schabi/newpipe/player/BasePlayer.java | 71 +- .../newpipe/player/MainVideoPlayer.java | 12 +- .../newpipe/player/PopupVideoPlayer.java | 4 +- .../newpipe/player/ServicePlayerActivity.java | 3 +- .../schabi/newpipe/player/VideoPlayer.java | 5 + .../util/CommentTextOnTouchListener.java | 2 +- .../schabi/newpipe/util/NavigationHelper.java | 47 +- .../newpipe/views/AnimatedProgressBar.java | 69 ++ .../progress_soundcloud_horizontal_dark.xml | 15 + .../progress_soundcloud_horizontal_light.xml | 15 + .../progress_youtube_horizontal_dark.xml | 15 + .../progress_youtube_horizontal_light.xml | 15 + .../fragment_video_detail.xml | 77 +- .../main/res/layout/fragment_video_detail.xml | 913 +++++++++--------- app/src/main/res/values-ru/strings.xml | 5 +- app/src/main/res/values-uk/strings.xml | 5 +- app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/settings_keys.xml | 1 + app/src/main/res/values/strings.xml | 5 +- app/src/main/res/values/styles.xml | 2 + app/src/main/res/values/styles_services.xml | 3 + app/src/main/res/xml/history_settings.xml | 58 +- 33 files changed, 978 insertions(+), 582 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/views/AnimatedProgressBar.java create mode 100644 app/src/main/res/drawable/progress_soundcloud_horizontal_dark.xml create mode 100644 app/src/main/res/drawable/progress_soundcloud_horizontal_light.xml create mode 100644 app/src/main/res/drawable/progress_youtube_horizontal_dark.xml create mode 100644 app/src/main/res/drawable/progress_youtube_horizontal_light.xml diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index f040dc8b4..c7bf4c881 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -574,7 +574,7 @@ public class RouterActivity extends AppCompatActivity { playQueue = new SinglePlayQueue((StreamInfo) info); if (playerChoice.equals(videoPlayerKey)) { - NavigationHelper.playOnMainPlayer(this, playQueue); + NavigationHelper.playOnMainPlayer(this, playQueue, true); } else if (playerChoice.equals(backgroundPlayerKey)) { NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true); } else if (playerChoice.equals(popupPlayerKey)) { @@ -587,11 +587,11 @@ public class RouterActivity extends AppCompatActivity { playQueue = info instanceof ChannelInfo ? new ChannelPlayQueue((ChannelInfo) info) : new PlaylistPlayQueue((PlaylistInfo) info); if (playerChoice.equals(videoPlayerKey)) { - NavigationHelper.playOnMainPlayer(this, playQueue); + NavigationHelper.playOnMainPlayer(this, playQueue, true); } else if (playerChoice.equals(backgroundPlayerKey)) { - NavigationHelper.playOnBackgroundPlayer(this, playQueue); + NavigationHelper.playOnBackgroundPlayer(this, playQueue, true); } else if (playerChoice.equals(popupPlayerKey)) { - NavigationHelper.playOnPopupPlayer(this, playQueue); + NavigationHelper.playOnPopupPlayer(this, playQueue, true); } } }; diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java index 847153e12..50d723f1f 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java @@ -50,6 +50,11 @@ public abstract class StreamHistoryDAO implements HistoryDAO> getHistory(); + @Query("SELECT * FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + + " = :streamId ORDER BY " + STREAM_ACCESS_DATE + " DESC LIMIT 1") + @Nullable + public abstract StreamHistoryEntity getLatestEntry(final long streamId); + @Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId") public abstract int deleteStreamHistory(final long streamId); diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java index 15940a964..946ee1182 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java @@ -5,6 +5,8 @@ import android.arch.persistence.room.ColumnInfo; import android.arch.persistence.room.Entity; import android.arch.persistence.room.ForeignKey; +import java.util.concurrent.TimeUnit; + import static android.arch.persistence.room.ForeignKey.CASCADE; import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID; import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; @@ -22,6 +24,12 @@ public class StreamStateEntity { final public static String JOIN_STREAM_ID = "stream_id"; final public static String STREAM_PROGRESS_TIME = "progress_time"; + + /** Playback state will not be saved, if playback time less than this threshold */ + private static final int PLAYBACK_SAVE_THRESHOLD_START_SECONDS = 5; + /** Playback state will not be saved, if time left less than this threshold */ + private static final int PLAYBACK_SAVE_THRESHOLD_END_SECONDS = 10; + @ColumnInfo(name = JOIN_STREAM_ID) private long streamUid; @@ -48,4 +56,10 @@ public class StreamStateEntity { public void setProgressTime(long progressTime) { this.progressTime = progressTime; } + + public boolean isValid(int durationInSeconds) { + final int seconds = (int) TimeUnit.MILLISECONDS.toSeconds(progressTime); + return seconds > PLAYBACK_SAVE_THRESHOLD_START_SECONDS + && seconds < durationInSeconds - PLAYBACK_SAVE_THRESHOLD_END_SECONDS; + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index bbd1a315d..d6630c9c3 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -89,12 +89,14 @@ import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.ShareUtils; import org.schabi.newpipe.util.StreamItemAdapter; import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper; +import org.schabi.newpipe.views.AnimatedProgressBar; import java.io.Serializable; import java.util.Collection; import java.util.Collections; import java.util.LinkedList; import java.util.List; +import java.util.concurrent.TimeUnit; import icepick.State; import io.reactivex.Single; @@ -118,7 +120,7 @@ public class VideoDetailFragment private static final int RELATED_STREAMS_UPDATE_FLAG = 0x1; private static final int RESOLUTIONS_MENU_UPDATE_FLAG = 0x2; private static final int TOOLBAR_ITEMS_UPDATE_FLAG = 0x4; - private static final int COMMENTS_UPDATE_FLAG = 0x4; + private static final int COMMENTS_UPDATE_FLAG = 0x8; private boolean autoPlayEnabled; private boolean showRelatedStreams; @@ -136,6 +138,8 @@ public class VideoDetailFragment private Disposable currentWorker; @NonNull private CompositeDisposable disposables = new CompositeDisposable(); + @Nullable + private Disposable positionSubscriber = null; private List sortedVideoStreams; private int selectedVideoStreamIndex = -1; @@ -153,6 +157,7 @@ public class VideoDetailFragment private View thumbnailBackgroundButton; private ImageView thumbnailImageView; private ImageView thumbnailPlayButton; + private AnimatedProgressBar positionView; private View videoTitleRoot; private TextView videoTitleTextView; @@ -165,6 +170,7 @@ public class VideoDetailFragment private TextView detailControlsDownload; private TextView appendControlsDetail; private TextView detailDurationView; + private TextView detailPositionView; private LinearLayout videoDescriptionRootLayout; private TextView videoUploadDateView; @@ -259,6 +265,8 @@ public class VideoDetailFragment // Check if it was loading when the fragment was stopped/paused, if (wasLoading.getAndSet(false)) { selectAndLoadVideo(serviceId, url, name); + } else if (currentInfo != null) { + updateProgressInfo(currentInfo); } } @@ -268,8 +276,10 @@ public class VideoDetailFragment PreferenceManager.getDefaultSharedPreferences(activity) .unregisterOnSharedPreferenceChangeListener(this); + if (positionSubscriber != null) positionSubscriber.dispose(); if (currentWorker != null) currentWorker.dispose(); if (disposables != null) disposables.clear(); + positionSubscriber = null; currentWorker = null; disposables = null; } @@ -462,6 +472,7 @@ public class VideoDetailFragment videoTitleTextView = rootView.findViewById(R.id.detail_video_title_view); videoTitleToggleArrow = rootView.findViewById(R.id.detail_toggle_description_view); videoCountView = rootView.findViewById(R.id.detail_view_count_view); + positionView = rootView.findViewById(R.id.position_view); detailControlsBackground = rootView.findViewById(R.id.detail_controls_background); detailControlsPopup = rootView.findViewById(R.id.detail_controls_popup); @@ -469,6 +480,7 @@ public class VideoDetailFragment detailControlsDownload = rootView.findViewById(R.id.detail_controls_download); appendControlsDetail = rootView.findViewById(R.id.touch_append_detail); detailDurationView = rootView.findViewById(R.id.detail_duration_view); + detailPositionView = rootView.findViewById(R.id.detail_position_view); videoDescriptionRootLayout = rootView.findViewById(R.id.detail_description_root_layout); videoUploadDateView = rootView.findViewById(R.id.detail_upload_date_view); @@ -536,10 +548,10 @@ public class VideoDetailFragment final DialogInterface.OnClickListener actions = (DialogInterface dialogInterface, int i) -> { switch (i) { case 0: - NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); + NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item), true); break; case 1: - NavigationHelper.enqueueOnPopupPlayer(getActivity(), new SinglePlayQueue(item)); + NavigationHelper.enqueueOnPopupPlayer(getActivity(), new SinglePlayQueue(item), true); break; case 2: if (getFragmentManager() != null) { @@ -890,11 +902,11 @@ public class VideoDetailFragment final PlayQueue itemQueue = new SinglePlayQueue(currentInfo); if (append) { - NavigationHelper.enqueueOnPopupPlayer(activity, itemQueue); + NavigationHelper.enqueueOnPopupPlayer(activity, itemQueue, false); } else { Toast.makeText(activity, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show(); final Intent intent = NavigationHelper.getPlayerIntent( - activity, PopupVideoPlayer.class, itemQueue, getSelectedVideoStream().resolution + activity, PopupVideoPlayer.class, itemQueue, getSelectedVideoStream().resolution, true ); activity.startService(intent); } @@ -914,9 +926,9 @@ public class VideoDetailFragment private void openNormalBackgroundPlayer(final boolean append) { final PlayQueue itemQueue = new SinglePlayQueue(currentInfo); if (append) { - NavigationHelper.enqueueOnBackgroundPlayer(activity, itemQueue); + NavigationHelper.enqueueOnBackgroundPlayer(activity, itemQueue, false); } else { - NavigationHelper.playOnBackgroundPlayer(activity, itemQueue); + NavigationHelper.playOnBackgroundPlayer(activity, itemQueue, true); } } @@ -926,7 +938,7 @@ public class VideoDetailFragment mIntent = NavigationHelper.getPlayerIntent(activity, MainVideoPlayer.class, playQueue, - getSelectedVideoStream().getResolution()); + getSelectedVideoStream().getResolution(), true); startActivity(mIntent); } @@ -1032,6 +1044,8 @@ public class VideoDetailFragment animateView(spinnerToolbar, false, 200); animateView(thumbnailPlayButton, false, 50); animateView(detailDurationView, false, 100); + animateView(detailPositionView, false, 100); + animateView(positionView, false, 50); videoTitleTextView.setText(name != null ? name : ""); videoTitleTextView.setMaxLines(1); @@ -1146,6 +1160,7 @@ public class VideoDetailFragment videoUploadDateView.setText(Localization.localizeDate(activity, info.getUploadDate())); } prepareDescription(info.getDescription()); + updateProgressInfo(info); animateView(spinnerToolbar, true, 500); setupActionBar(info); @@ -1250,4 +1265,37 @@ public class VideoDetailFragment showError(getString(R.string.blocked_by_gema), false, R.drawable.gruese_die_gema); } + + private void updateProgressInfo(@NonNull final StreamInfo info) { + if (positionSubscriber != null) { + positionSubscriber.dispose(); + } + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity); + final boolean playbackResumeEnabled = + prefs.getBoolean(activity.getString(R.string.enable_watch_history_key), true) + && prefs.getBoolean(activity.getString(R.string.enable_playback_resume_key), true); + if (!playbackResumeEnabled || info.getDuration() <= 0) { + positionView.setVisibility(View.INVISIBLE); + detailPositionView.setVisibility(View.GONE); + return; + } + final HistoryRecordManager recordManager = new HistoryRecordManager(requireContext()); + positionSubscriber = recordManager.loadStreamState(info) + .subscribeOn(Schedulers.io()) + .onErrorComplete() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(state -> { + final int seconds = (int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()); + positionView.setMax((int) info.getDuration()); + positionView.setProgress(seconds); + detailPositionView.setText(Localization.getDurationString(seconds)); + animateView(positionView, true, 500); + animateView(detailPositionView, true, 500); + }, e -> { + if (DEBUG) e.printStackTrace(); + }, () -> { + animateView(positionView, false, 500); + animateView(detailPositionView, false, 500); + }); + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java index dbc3dd8a2..68784852e 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -266,13 +266,13 @@ public abstract class BaseListFragment extends BaseStateFragment implem final DialogInterface.OnClickListener actions = (dialogInterface, i) -> { switch (i) { case 0: - NavigationHelper.playOnBackgroundPlayer(context, new SinglePlayQueue(item)); + NavigationHelper.playOnBackgroundPlayer(context, new SinglePlayQueue(item), true); break; case 1: - NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); + NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item), true); break; case 2: - NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); + NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item), true); break; case 3: if (getFragmentManager() != null) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 71865b04d..934e934e9 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -170,19 +170,19 @@ public class ChannelFragment extends BaseListInfoFragment { final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0); switch (i) { case 0: - NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); + NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item), false); break; case 1: - NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); + NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item), false); break; case 2: - NavigationHelper.playOnMainPlayer(context, getPlayQueue(index)); + NavigationHelper.playOnMainPlayer(context, getPlayQueue(index), true); break; case 3: - NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index)); + NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index), true); break; case 4: - NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index)); + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index), true); break; case 5: if (getFragmentManager() != null) { @@ -440,11 +440,11 @@ public class ChannelFragment extends BaseListInfoFragment { monitorSubscription(result); headerPlayAllButton.setOnClickListener( - view -> NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); + view -> NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false)); headerPopupButton.setOnClickListener( - view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue())); + view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false)); headerBackgroundButton.setOnClickListener( - view -> NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue())); + view -> NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false)); } private PlayQueue getPlayQueue() { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index 2a775fe8f..77aa0a250 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -154,22 +154,22 @@ public class PlaylistFragment extends BaseListInfoFragment { final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0); switch (i) { case 0: - NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item)); + NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item), false); break; case 1: - NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item)); + NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item), false); break; case 2: - NavigationHelper.playOnMainPlayer(context, getPlayQueue(index)); + NavigationHelper.playOnMainPlayer(context, getPlayQueue(index), true); break; case 3: - NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index)); + NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index), true); break; case 4: - NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index)); + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index), true); break; case 5: - ShareUtils.shareUrl(this.getContext(), item.getName(), item.getUrl()); + ShareUtils.shareUrl(requireContext(), item.getName(), item.getUrl()); break; default: break; @@ -301,19 +301,19 @@ public class PlaylistFragment extends BaseListInfoFragment { .subscribe(getPlaylistBookmarkSubscriber()); headerPlayAllButton.setOnClickListener(view -> - NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); + NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false)); headerPopupButton.setOnClickListener(view -> - NavigationHelper.playOnPopupPlayer(activity, getPlayQueue())); + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false)); headerBackgroundButton.setOnClickListener(view -> - NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue())); + NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false)); headerPopupButton.setOnLongClickListener(view -> { - NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue()); + NavigationHelper.enqueueOnPopupPlayer(activity, getPlayQueue(), true); return true; }); headerBackgroundButton.setOnLongClickListener(view -> { - NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue()); + NavigationHelper.enqueueOnBackgroundPlayer(activity, getPlayQueue(), true); return true; }); } diff --git a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java index 56453773a..f9090128c 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java @@ -37,12 +37,14 @@ import org.schabi.newpipe.database.stream.dao.StreamStateDAO; import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.player.playqueue.PlayQueueItem; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.List; +import io.reactivex.Completable; import io.reactivex.Flowable; import io.reactivex.Maybe; import io.reactivex.Single; @@ -80,9 +82,9 @@ public class HistoryRecordManager { final Date currentTime = new Date(); return Maybe.fromCallable(() -> database.runInTransaction(() -> { final long streamId = streamTable.upsert(new StreamEntity(info)); - StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(); + StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(streamId); - if (latestEntry != null && latestEntry.getStreamUid() == streamId) { + if (latestEntry != null) { streamHistoryTable.delete(latestEntry); latestEntry.setAccessDate(currentTime); latestEntry.setRepeatCount(latestEntry.getRepeatCount() + 1); @@ -99,7 +101,7 @@ public class HistoryRecordManager { } public Single deleteWholeStreamHistory() { - return Single.fromCallable(() -> streamHistoryTable.deleteAll()) + return Single.fromCallable(streamHistoryTable::deleteAll) .subscribeOn(Schedulers.io()); } @@ -160,7 +162,7 @@ public class HistoryRecordManager { } public Single deleteWholeSearchHistory() { - return Single.fromCallable(() -> searchHistoryTable.deleteAll()) + return Single.fromCallable(searchHistoryTable::deleteAll) .subscribeOn(Schedulers.io()); } @@ -180,18 +182,41 @@ public class HistoryRecordManager { // Stream State History /////////////////////////////////////////////////////// - @SuppressWarnings("unused") - public Maybe loadStreamState(final StreamInfo info) { - return Maybe.fromCallable(() -> streamTable.upsert(new StreamEntity(info))) - .flatMap(streamId -> streamStateTable.getState(streamId).firstElement()) - .flatMap(states -> states.isEmpty() ? Maybe.empty() : Maybe.just(states.get(0))) + public Maybe getStreamHistory(final StreamInfo info) { + return Maybe.fromCallable(() -> { + final long streamId = streamTable.upsert(new StreamEntity(info)); + return streamHistoryTable.getLatestEntry(streamId); + }).subscribeOn(Schedulers.io()); + } + + public Maybe loadStreamState(final PlayQueueItem queueItem) { + return queueItem.getStream() + .map((info) -> streamTable.upsert(new StreamEntity(info))) + .flatMapPublisher(streamStateTable::getState) + .firstElement() + .flatMap(list -> list.isEmpty() ? Maybe.empty() : Maybe.just(list.get(0))) + .filter(state -> state.isValid((int) queueItem.getDuration())) .subscribeOn(Schedulers.io()); } - public Maybe saveStreamState(@NonNull final StreamInfo info, final long progressTime) { - return Maybe.fromCallable(() -> database.runInTransaction(() -> { + public Maybe loadStreamState(final StreamInfo info) { + return Single.fromCallable(() -> streamTable.upsert(new StreamEntity(info))) + .flatMapPublisher(streamStateTable::getState) + .firstElement() + .flatMap(list -> list.isEmpty() ? Maybe.empty() : Maybe.just(list.get(0))) + .filter(state -> state.isValid((int) info.getDuration())) + .subscribeOn(Schedulers.io()); + } + + public Completable saveStreamState(@NonNull final StreamInfo info, final long progressTime) { + return Completable.fromAction(() -> database.runInTransaction(() -> { final long streamId = streamTable.upsert(new StreamEntity(info)); - return streamStateTable.upsert(new StreamStateEntity(streamId, progressTime)); + final StreamStateEntity state = new StreamStateEntity(streamId, progressTime); + if (state.isValid((int) info.getDuration())) { + streamStateTable.upsert(state); + } else { + streamStateTable.deleteState(streamId); + } })).subscribeOn(Schedulers.io()); } diff --git a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java index 5a62a3969..b97fdf8f2 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java @@ -310,11 +310,11 @@ public class StatisticsPlaylistFragment } headerPlayAllButton.setOnClickListener(view -> - NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); + NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false)); headerPopupButton.setOnClickListener(view -> - NavigationHelper.playOnPopupPlayer(activity, getPlayQueue())); + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false)); headerBackgroundButton.setOnClickListener(view -> - NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue())); + NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false)); sortButton.setOnClickListener(view -> toggleSortMode()); hideLoading(); @@ -377,19 +377,19 @@ public class StatisticsPlaylistFragment final int index = Math.max(itemListAdapter.getItemsList().indexOf(item), 0); switch (i) { case 0: - NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(infoItem)); + NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(infoItem), false); break; case 1: - NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(infoItem)); + NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(infoItem), false); break; case 2: - NavigationHelper.playOnMainPlayer(context, getPlayQueue(index)); + NavigationHelper.playOnMainPlayer(context, getPlayQueue(index), true); break; case 3: - NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index)); + NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index), true); break; case 4: - NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index)); + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index), true); break; case 5: deleteEntry(index); diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java index dc101fade..501016642 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java @@ -319,11 +319,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment - NavigationHelper.playOnMainPlayer(activity, getPlayQueue())); + NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false)); headerPopupButton.setOnClickListener(view -> - NavigationHelper.playOnPopupPlayer(activity, getPlayQueue())); + NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false)); headerBackgroundButton.setOnClickListener(view -> - NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue())); + NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false)); hideLoading(); } @@ -534,20 +534,20 @@ public class LocalPlaylistFragment extends BaseLocalListFragment initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence, + /*playOnInit=*/true)) + .subscribe( + state -> queue.setRecovery(queue.getIndex(), state.getProgressTime()), + error -> { + if (DEBUG) error.printStackTrace(); + } + ); + databaseUpdateReactor.add(stateLoader); + return; + } } - // Good to go... initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence, /*playOnInit=*/true); @@ -615,6 +634,9 @@ public abstract class BasePlayer implements break; case Player.STATE_ENDED: // 4 changeState(STATE_COMPLETED); + if (currentMetadata != null) { + resetPlaybackState(currentMetadata.getMetadata()); + } isPrepared = false; break; } @@ -721,6 +743,7 @@ public abstract class BasePlayer implements case DISCONTINUITY_REASON_SEEK_ADJUSTMENT: case DISCONTINUITY_REASON_INTERNAL: if (playQueue.getIndex() != newWindowIndex) { + resetPlaybackState(playQueue.getItem()); playQueue.setIndex(newWindowIndex); } break; @@ -750,6 +773,9 @@ public abstract class BasePlayer implements @Override public void onSeekProcessed() { if (DEBUG) Log.d(TAG, "ExoPlayer - onSeekProcessed() called"); + if (isPrepared) { + savePlaybackState(); + } } /*////////////////////////////////////////////////////////////////////////// // Playback Listener @@ -1017,27 +1043,40 @@ public abstract class BasePlayer implements } } - protected void savePlaybackState(final StreamInfo info, final long progress) { + private void savePlaybackState(final StreamInfo info, final long progress) { if (info == null) return; + if (DEBUG) Log.d(TAG, "savePlaybackState() called"); final Disposable stateSaver = recordManager.saveStreamState(info, progress) .observeOn(AndroidSchedulers.mainThread()) + .doOnError((e) -> { + if (DEBUG) e.printStackTrace(); + }) .onErrorComplete() - .subscribe( - ignored -> {/* successful */}, - error -> Log.e(TAG, "savePlaybackState() failure: ", error) - ); + .subscribe(); databaseUpdateReactor.add(stateSaver); } - private void savePlaybackState() { + private void resetPlaybackState(final PlayQueueItem queueItem) { + if (queueItem == null) return; + final Disposable stateSaver = queueItem.getStream() + .flatMapCompletable(info -> recordManager.saveStreamState(info, 0)) + .observeOn(AndroidSchedulers.mainThread()) + .doOnError((e) -> { + if (DEBUG) e.printStackTrace(); + }) + .onErrorComplete() + .subscribe(); + databaseUpdateReactor.add(stateSaver); + } + + public void resetPlaybackState(final StreamInfo info) { + savePlaybackState(info, 0); + } + + public void savePlaybackState() { if (simpleExoPlayer == null || currentMetadata == null) return; final StreamInfo currentInfo = currentMetadata.getMetadata(); - - if (simpleExoPlayer.getCurrentPosition() > RECOVERY_SKIP_THRESHOLD_MILLIS && - simpleExoPlayer.getCurrentPosition() < - simpleExoPlayer.getDuration() - RECOVERY_SKIP_THRESHOLD_MILLIS) { - savePlaybackState(currentInfo, simpleExoPlayer.getCurrentPosition()); - } + savePlaybackState(currentInfo, simpleExoPlayer.getCurrentPosition()); } private void maybeUpdateCurrentMetadata() { @@ -1225,4 +1264,10 @@ public abstract class BasePlayer implements public boolean gotDestroyed() { return simpleExoPlayer == null; } + + private boolean isPlaybackResumeEnabled() { + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + return prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true) + && prefs.getBoolean(context.getString(R.string.enable_playback_resume_key), true); + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index cf179917d..902ee5065 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -247,6 +247,12 @@ public final class MainVideoPlayer extends AppCompatActivity super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(newBase)); } + @Override + protected void onPause() { + playerImpl.savePlaybackState(); + super.onPause(); + } + /*////////////////////////////////////////////////////////////////////////// // State Saving //////////////////////////////////////////////////////////////////////////*/ @@ -579,7 +585,8 @@ public final class MainVideoPlayer extends AppCompatActivity this.getPlaybackSpeed(), this.getPlaybackPitch(), this.getPlaybackSkipSilence(), - this.getPlaybackQuality() + this.getPlaybackQuality(), + false ); context.startService(intent); @@ -601,7 +608,8 @@ public final class MainVideoPlayer extends AppCompatActivity this.getPlaybackSpeed(), this.getPlaybackPitch(), this.getPlaybackSkipSilence(), - this.getPlaybackQuality() + this.getPlaybackQuality(), + false ); context.startService(intent); diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index 7578c444c..3782d85c0 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -325,6 +325,7 @@ public final class PopupVideoPlayer extends Service { isPopupClosing = true; if (playerImpl != null) { + playerImpl.savePlaybackState(); if (playerImpl.getRootView() != null) { windowManager.removeView(playerImpl.getRootView()); } @@ -565,7 +566,8 @@ public final class PopupVideoPlayer extends Service { this.getPlaybackSpeed(), this.getPlaybackPitch(), this.getPlaybackSkipSilence(), - this.getPlaybackQuality() + this.getPlaybackQuality(), + false ); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index 3e04f1e3a..bdd31f21b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -188,7 +188,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity this.player.getPlaybackSpeed(), this.player.getPlaybackPitch(), this.player.getPlaybackSkipSilence(), - null + null, + false ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java index fb60ac473..bbfe805a5 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -543,6 +543,11 @@ public abstract class VideoPlayer extends BasePlayer playbackSpeedTextView.setText(formatSpeed(getPlaybackSpeed())); super.onPrepared(playWhenReady); + + if (simpleExoPlayer.getCurrentPosition() != 0 && !isControlsVisible()) { + controlsVisibilityHandler.removeCallbacksAndMessages(null); + controlsVisibilityHandler.postDelayed(this::showControlsThenHide, DEFAULT_CONTROLS_DURATION); + } } @Override diff --git a/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java b/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java index e1ecc662d..ac79fee23 100644 --- a/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java +++ b/app/src/main/java/org/schabi/newpipe/util/CommentTextOnTouchListener.java @@ -124,7 +124,7 @@ public class CommentTextOnTouchListener implements View.OnTouchListener { .observeOn(AndroidSchedulers.mainThread()) .subscribe(info -> { PlayQueue playQueue = new SinglePlayQueue((StreamInfo) info, seconds*1000); - NavigationHelper.playOnPopupPlayer(context, playQueue); + NavigationHelper.playOnPopupPlayer(context, playQueue, false); }); return true; } diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index 98ae3a88a..89c4b33fe 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -69,12 +69,14 @@ public class NavigationHelper { public static Intent getPlayerIntent(@NonNull final Context context, @NonNull final Class targetClazz, @NonNull final PlayQueue playQueue, - @Nullable final String quality) { + @Nullable final String quality, + final boolean resumePlayback) { Intent intent = new Intent(context, targetClazz); final String cacheKey = SerializedCache.getInstance().put(playQueue, PlayQueue.class); if (cacheKey != null) intent.putExtra(VideoPlayer.PLAY_QUEUE_KEY, cacheKey); if (quality != null) intent.putExtra(VideoPlayer.PLAYBACK_QUALITY, quality); + intent.putExtra(VideoPlayer.RESUME_PLAYBACK, resumePlayback); return intent; } @@ -82,16 +84,18 @@ public class NavigationHelper { @NonNull public static Intent getPlayerIntent(@NonNull final Context context, @NonNull final Class targetClazz, - @NonNull final PlayQueue playQueue) { - return getPlayerIntent(context, targetClazz, playQueue, null); + @NonNull final PlayQueue playQueue, + final boolean resumePlayback) { + return getPlayerIntent(context, targetClazz, playQueue, null, resumePlayback); } @NonNull public static Intent getPlayerEnqueueIntent(@NonNull final Context context, @NonNull final Class targetClazz, @NonNull final PlayQueue playQueue, - final boolean selectOnAppend) { - return getPlayerIntent(context, targetClazz, playQueue) + final boolean selectOnAppend, + final boolean resumePlayback) { + return getPlayerIntent(context, targetClazz, playQueue, resumePlayback) .putExtra(BasePlayer.APPEND_ONLY, true) .putExtra(BasePlayer.SELECT_ON_APPEND, selectOnAppend); } @@ -104,40 +108,41 @@ public class NavigationHelper { final float playbackSpeed, final float playbackPitch, final boolean playbackSkipSilence, - @Nullable final String playbackQuality) { - return getPlayerIntent(context, targetClazz, playQueue, playbackQuality) + @Nullable final String playbackQuality, + final boolean resumePlayback) { + return getPlayerIntent(context, targetClazz, playQueue, playbackQuality, resumePlayback) .putExtra(BasePlayer.REPEAT_MODE, repeatMode) .putExtra(BasePlayer.PLAYBACK_SPEED, playbackSpeed) .putExtra(BasePlayer.PLAYBACK_PITCH, playbackPitch) .putExtra(BasePlayer.PLAYBACK_SKIP_SILENCE, playbackSkipSilence); } - public static void playOnMainPlayer(final Context context, final PlayQueue queue) { - final Intent playerIntent = getPlayerIntent(context, MainVideoPlayer.class, queue); + public static void playOnMainPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) { + final Intent playerIntent = getPlayerIntent(context, MainVideoPlayer.class, queue, resumePlayback); playerIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(playerIntent); } - public static void playOnPopupPlayer(final Context context, final PlayQueue queue) { + public static void playOnPopupPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) { if (!PermissionHelper.isPopupEnabled(context)) { PermissionHelper.showPopupEnablementToast(context); return; } Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show(); - startService(context, getPlayerIntent(context, PopupVideoPlayer.class, queue)); + startService(context, getPlayerIntent(context, PopupVideoPlayer.class, queue, resumePlayback)); } - public static void playOnBackgroundPlayer(final Context context, final PlayQueue queue) { + public static void playOnBackgroundPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) { Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT).show(); - startService(context, getPlayerIntent(context, BackgroundPlayer.class, queue)); + startService(context, getPlayerIntent(context, BackgroundPlayer.class, queue, resumePlayback)); } - public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue) { - enqueueOnPopupPlayer(context, queue, false); + public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) { + enqueueOnPopupPlayer(context, queue, false, resumePlayback); } - public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue, boolean selectOnAppend) { + public static void enqueueOnPopupPlayer(final Context context, final PlayQueue queue, boolean selectOnAppend, final boolean resumePlayback) { if (!PermissionHelper.isPopupEnabled(context)) { PermissionHelper.showPopupEnablementToast(context); return; @@ -145,17 +150,17 @@ public class NavigationHelper { Toast.makeText(context, R.string.popup_playing_append, Toast.LENGTH_SHORT).show(); startService(context, - getPlayerEnqueueIntent(context, PopupVideoPlayer.class, queue, selectOnAppend)); + getPlayerEnqueueIntent(context, PopupVideoPlayer.class, queue, selectOnAppend, resumePlayback)); } - public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue) { - enqueueOnBackgroundPlayer(context, queue, false); + public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) { + enqueueOnBackgroundPlayer(context, queue, false, resumePlayback); } - public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue, boolean selectOnAppend) { + public static void enqueueOnBackgroundPlayer(final Context context, final PlayQueue queue, boolean selectOnAppend, final boolean resumePlayback) { Toast.makeText(context, R.string.background_player_append, Toast.LENGTH_SHORT).show(); startService(context, - getPlayerEnqueueIntent(context, BackgroundPlayer.class, queue, selectOnAppend)); + getPlayerEnqueueIntent(context, BackgroundPlayer.class, queue, selectOnAppend, resumePlayback)); } public static void startService(@NonNull final Context context, @NonNull final Intent intent) { diff --git a/app/src/main/java/org/schabi/newpipe/views/AnimatedProgressBar.java b/app/src/main/java/org/schabi/newpipe/views/AnimatedProgressBar.java new file mode 100644 index 000000000..39c6e707e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/views/AnimatedProgressBar.java @@ -0,0 +1,69 @@ +package org.schabi.newpipe.views; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.Animation; +import android.view.animation.Transformation; +import android.widget.ProgressBar; + +public final class AnimatedProgressBar extends ProgressBar { + + @Nullable + private ProgressBarAnimation animation = null; + + public AnimatedProgressBar(Context context) { + super(context); + } + + public AnimatedProgressBar(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public AnimatedProgressBar(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + public synchronized void setProgress(int progress) { + cancelAnimation(); + animation = new ProgressBarAnimation(this, getProgress(), progress); + startAnimation(animation); + } + + private void cancelAnimation() { + if (animation != null) { + animation.cancel(); + animation = null; + } + clearAnimation(); + } + + private void setProgressInternal(int progress) { + super.setProgress(progress); + } + + private static class ProgressBarAnimation extends Animation { + + private final AnimatedProgressBar progressBar; + private final float from; + private final float to; + + ProgressBarAnimation(AnimatedProgressBar progressBar, float from, float to) { + super(); + this.progressBar = progressBar; + this.from = from; + this.to = to; + setDuration(500); + setInterpolator(new AccelerateDecelerateInterpolator()); + } + + @Override + protected void applyTransformation(float interpolatedTime, Transformation t) { + super.applyTransformation(interpolatedTime, t); + float value = from + (to - from) * interpolatedTime; + progressBar.setProgressInternal((int) value); + } + } +} diff --git a/app/src/main/res/drawable/progress_soundcloud_horizontal_dark.xml b/app/src/main/res/drawable/progress_soundcloud_horizontal_dark.xml new file mode 100644 index 000000000..1ec9f67b6 --- /dev/null +++ b/app/src/main/res/drawable/progress_soundcloud_horizontal_dark.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/progress_soundcloud_horizontal_light.xml b/app/src/main/res/drawable/progress_soundcloud_horizontal_light.xml new file mode 100644 index 000000000..c326c5c04 --- /dev/null +++ b/app/src/main/res/drawable/progress_soundcloud_horizontal_light.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/progress_youtube_horizontal_dark.xml b/app/src/main/res/drawable/progress_youtube_horizontal_dark.xml new file mode 100644 index 000000000..404410f98 --- /dev/null +++ b/app/src/main/res/drawable/progress_youtube_horizontal_dark.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/progress_youtube_horizontal_light.xml b/app/src/main/res/drawable/progress_youtube_horizontal_light.xml new file mode 100644 index 000000000..120a6e5fb --- /dev/null +++ b/app/src/main/res/drawable/progress_youtube_horizontal_light.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-large-land/fragment_video_detail.xml b/app/src/main/res/layout-large-land/fragment_video_detail.xml index 8cdc2f307..a8fbcca89 100644 --- a/app/src/main/res/layout-large-land/fragment_video_detail.xml +++ b/app/src/main/res/layout-large-land/fragment_video_detail.xml @@ -1,7 +1,7 @@ @@ -67,10 +68,10 @@ android:layout_height="wrap_content" android:layout_gravity="center" android:background="#64000000" - android:paddingBottom="10dp" android:paddingLeft="30dp" - android:paddingRight="30dp" android:paddingTop="10dp" + android:paddingRight="30dp" + android:paddingBottom="10dp" android:text="@string/hold_to_append" android:textColor="@android:color/white" android:textSize="20sp" @@ -84,17 +85,42 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|right" - android:layout_marginBottom="8dp" android:layout_marginLeft="12dp" - android:layout_marginRight="12dp" android:layout_marginTop="8dp" + android:layout_marginRight="12dp" + android:layout_marginBottom="8dp" android:alpha=".6" android:background="#23000000" android:gravity="center" - android:paddingBottom="2dp" android:paddingLeft="6dp" - android:paddingRight="6dp" android:paddingTop="2dp" + android:paddingRight="6dp" + android:paddingBottom="2dp" + android:textAllCaps="true" + android:textColor="@android:color/white" + android:textSize="12sp" + android:textStyle="bold" + android:visibility="gone" + tools:ignore="RtlHardcoded" + tools:text="12:38" + tools:visibility="visible" /> + + + + @@ -191,8 +230,8 @@ android:layout_width="match_parent" android:layout_height="55dp" android:layout_marginLeft="12dp" - android:layout_marginRight="12dp" android:layout_marginTop="6dp" + android:layout_marginRight="12dp" android:baselineAligned="false" android:orientation="horizontal"> @@ -201,8 +240,8 @@ android:id="@+id/detail_uploader_root_layout" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_toLeftOf="@id/details_panel" android:layout_toStartOf="@id/details_panel" + android:layout_toLeftOf="@id/details_panel" android:background="?attr/selectableItemBackground" android:gravity="center_vertical" android:orientation="horizontal" @@ -261,8 +300,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" - android:layout_marginBottom="6dp" android:layout_marginTop="6dp" + android:layout_marginBottom="6dp" android:lines="1" android:textAppearance="?android:attr/textAppearanceLarge" android:textSize="@dimen/video_item_detail_views_text_size" @@ -354,8 +393,8 @@ android:drawableTop="?attr/ic_playlist_add" android:focusable="true" android:gravity="center" - android:paddingBottom="6dp" android:paddingTop="6dp" + android:paddingBottom="6dp" android:text="@string/controls_add_to_playlist_title" android:textSize="12sp" /> @@ -371,8 +410,8 @@ android:drawableTop="?attr/audio" android:focusable="true" android:gravity="center" - android:paddingBottom="6dp" android:paddingTop="6dp" + android:paddingBottom="6dp" android:text="@string/controls_background_title" android:textSize="12sp" /> @@ -388,8 +427,8 @@ android:drawableTop="?attr/popup" android:focusable="true" android:gravity="center" - android:paddingBottom="6dp" android:paddingTop="6dp" + android:paddingBottom="6dp" android:text="@string/controls_popup_title" android:textSize="12sp" /> @@ -405,8 +444,8 @@ android:drawableTop="?attr/download" android:focusable="true" android:gravity="center" - android:paddingBottom="6dp" android:paddingTop="6dp" + android:paddingBottom="6dp" android:text="@string/download" android:textSize="12sp" /> @@ -444,10 +483,10 @@ android:id="@+id/detail_description_view" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginBottom="8dp" android:layout_marginLeft="12dp" - android:layout_marginRight="12dp" android:layout_marginTop="3dp" + android:layout_marginRight="12dp" + android:layout_marginBottom="8dp" android:textAppearance="?android:attr/textAppearanceMedium" android:textIsSelectable="true" android:textSize="@dimen/video_item_detail_description_text_size" @@ -490,7 +529,7 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/video_item_detail" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:focusableInTouchMode="true"> - + - + - + - - + + - + - + - + - - + - + - - + - - + - + - + + - + + - - + - - + - - + - - + + - - + + - + + - + + - - + + - - + - + - + + - + + - + - + - - - + - + - - + - + + + - + - + + - + - + - - + - + - + - + + - + - - + - + + + + + + + + - + - + - + - + diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 32d57db8b..759c24460 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -167,7 +167,10 @@ Что нового История поиска Хранить запросы поиска локально - История и кэш + История просмотров + Продолжать воспроизведение + Восстанавливать с последней позиции + Очистить данные Запоминать воспроизведённые потоки Возобновить при фокусе Возобновлять воспроизведение после перерывов (например, телефонных звонков) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index eaca5719a..41e6f22ba 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -153,7 +153,10 @@ Показувати пропозиції під час пошуку Історія пошуків Зберігати пошукові запити локально - Історія та кеш + Історія переглядiв + Продовживати перегляд + Відновлювати останню позицію + Очистити дані Вести облік перегляду відеозаписів Відновити відтворення Продовжувати відтворення опісля переривання (наприклад телефонного дзвінка) diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 865b68c24..bdf42c4ed 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -43,6 +43,7 @@ + diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 3861f53d5..b70305d56 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -150,6 +150,7 @@ enable_search_history enable_watch_history main_page_content + enable_playback_resume import_data export_data diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 98a32d9e6..a84d7db36 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -95,7 +95,10 @@ Show suggestions when searching Search history Store search queries locally - History & Cache + Watch history + Resume playback + Restore last playback position + Clear data Keep track of watched videos Resume on focus gain Continue playing after interruptions (e.g. phone calls) diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index a7686dedc..6df9069ff 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -65,6 +65,7 @@ @drawable/toolbar_shadow_light @drawable/light_selector @color/light_ripple_color + @drawable/progress_youtube_horizontal_light @style/PreferenceThemeOverlay.v14.Material @@ -128,6 +129,7 @@ @drawable/toolbar_shadow_dark @drawable/dark_selector @color/dark_ripple_color + @drawable/progress_youtube_horizontal_dark @style/PreferenceThemeOverlay.v14.Material diff --git a/app/src/main/res/values/styles_services.xml b/app/src/main/res/values/styles_services.xml index 257b1905d..d6ab239e4 100644 --- a/app/src/main/res/values/styles_services.xml +++ b/app/src/main/res/values/styles_services.xml @@ -15,18 +15,21 @@ @color/light_soundcloud_primary_color @color/light_soundcloud_dark_color @color/light_soundcloud_accent_color + @drawable/progress_soundcloud_horizontal_light diff --git a/app/src/main/res/xml/history_settings.xml b/app/src/main/res/xml/history_settings.xml index a7428d340..305b1c360 100644 --- a/app/src/main/res/xml/history_settings.xml +++ b/app/src/main/res/xml/history_settings.xml @@ -1,40 +1,54 @@ - + android:title="@string/enable_watch_history_title" + app:iconSpaceReserved="false" /> + + + android:title="@string/enable_search_history_title" + app:iconSpaceReserved="false" /> - + - + - + - + + + + + \ No newline at end of file From 002a1412cbd122328ad14bcfd9d559a40de01de0 Mon Sep 17 00:00:00 2001 From: Vasiliy Date: Mon, 15 Apr 2019 21:22:31 +0300 Subject: [PATCH 068/138] Fix scrolling details --- app/src/main/res/layout-large-land/fragment_video_detail.xml | 1 + app/src/main/res/layout/fragment_video_detail.xml | 1 + 2 files changed, 2 insertions(+) diff --git a/app/src/main/res/layout-large-land/fragment_video_detail.xml b/app/src/main/res/layout-large-land/fragment_video_detail.xml index a8fbcca89..f773c69cf 100644 --- a/app/src/main/res/layout-large-land/fragment_video_detail.xml +++ b/app/src/main/res/layout-large-land/fragment_video_detail.xml @@ -142,6 +142,7 @@ android:background="@android:color/transparent" android:progressDrawable="?attr/progress_horizontal_drawable" android:visibility="invisible" + app:layout_scrollFlags="scroll" tools:max="100" tools:progress="40" tools:visibility="visible" /> diff --git a/app/src/main/res/layout/fragment_video_detail.xml b/app/src/main/res/layout/fragment_video_detail.xml index 1dfc61cf7..2139fd0cc 100644 --- a/app/src/main/res/layout/fragment_video_detail.xml +++ b/app/src/main/res/layout/fragment_video_detail.xml @@ -140,6 +140,7 @@ android:layout_marginTop="-2dp" android:progressDrawable="?attr/progress_horizontal_drawable" android:visibility="invisible" + app:layout_scrollFlags="scroll" tools:max="100" tools:progress="40" tools:visibility="visible" /> From 73be8cf074cf164b478d5785dc5c1e40ac14e999 Mon Sep 17 00:00:00 2001 From: Vasiliy Date: Mon, 15 Apr 2019 21:19:59 +0300 Subject: [PATCH 069/138] Base implementation of showing playback positions in lists --- .../fragments/list/BaseListFragment.java | 6 +++ .../newpipe/info_list/InfoItemBuilder.java | 11 +++-- .../newpipe/info_list/InfoListAdapter.java | 44 +++++++++++++++++-- .../holder/ChannelInfoItemHolder.java | 6 ++- .../holder/ChannelMiniInfoItemHolder.java | 4 +- .../holder/CommentsInfoItemHolder.java | 8 ++-- .../holder/CommentsMiniInfoItemHolder.java | 4 +- .../info_list/holder/InfoItemHolder.java | 4 +- .../holder/PlaylistMiniInfoItemHolder.java | 4 +- .../holder/StreamInfoItemHolder.java | 6 ++- .../holder/StreamMiniInfoItemHolder.java | 18 +++++++- .../local/history/HistoryRecordManager.java | 35 +++++++++++++++ .../subscription/SubscriptionFragment.java | 9 ++-- .../main/res/layout/list_stream_grid_item.xml | 12 +++++ app/src/main/res/layout/list_stream_item.xml | 12 +++++ .../main/res/layout/list_stream_mini_item.xml | 12 +++++ 16 files changed, 172 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java index 68784852e..5a49cce28 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -65,6 +65,12 @@ public abstract class BaseListFragment extends BaseStateFragment implem infoListAdapter = new InfoListAdapter(activity); } + @Override + public void onDetach() { + infoListAdapter.dispose(); + super.onDetach(); + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java index 0e9fd3277..39f7971dd 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java @@ -2,12 +2,14 @@ package org.schabi.newpipe.info_list; import android.content.Context; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.Log; import android.view.View; import android.view.ViewGroup; import com.nostra13.universalimageloader.core.ImageLoader; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; import org.schabi.newpipe.extractor.comments.CommentsInfoItem; @@ -59,13 +61,14 @@ public class InfoItemBuilder { this.context = context; } - public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem) { - return buildView(parent, infoItem, false); + public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem, @Nullable StreamStateEntity state) { + return buildView(parent, infoItem, state, false); } - public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem, boolean useMiniVariant) { + public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem, + @Nullable StreamStateEntity state, boolean useMiniVariant) { InfoItemHolder holder = holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant); - holder.updateFromItem(infoItem); + holder.updateFromItem(infoItem, state); return holder.itemView; } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java index 5e7095c7d..8e54f582a 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java @@ -7,16 +7,17 @@ import android.util.Log; import android.view.View; import android.view.ViewGroup; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; import org.schabi.newpipe.extractor.comments.CommentsInfoItem; import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder; import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder; import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder; import org.schabi.newpipe.info_list.holder.CommentsInfoItemHolder; import org.schabi.newpipe.info_list.holder.CommentsMiniInfoItemHolder; -import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder; import org.schabi.newpipe.info_list.holder.InfoItemHolder; import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder; import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder; @@ -24,12 +25,16 @@ import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamGridInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder; +import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.util.FallbackViewHolder; import org.schabi.newpipe.util.OnClickGesture; import java.util.ArrayList; import java.util.List; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; + /* * Created by Christian Schabesberger on 01.08.16. * @@ -70,7 +75,10 @@ public class InfoListAdapter extends RecyclerView.Adapter infoItemList; + private final ArrayList states; + private final CompositeDisposable stateLoaders; private boolean useMiniVariant = false; private boolean useGridVariant = false; private boolean showFooter = false; @@ -88,7 +96,10 @@ public class InfoListAdapter extends RecyclerView.Adapter(); + states = new ArrayList<>(); + stateLoaders = new CompositeDisposable(); } public void setOnStreamSelectedListener(OnClickGesture listener) { @@ -115,7 +126,17 @@ public class InfoListAdapter extends RecyclerView.Adapter data) { + public void addInfoItemList(final List data) { + stateLoaders.add( + historyRecordManager.loadStreamStateBatch(data) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(streamStateEntities -> { + addInfoItemList(data, streamStateEntities); + }) + ); + } + + private void addInfoItemList(List data, List statesEntities) { if (data != null) { if (DEBUG) { Log.d(TAG, "addInfoItemList() before > infoItemList.size() = " + infoItemList.size() + ", data.size() = " + data.size()); @@ -123,6 +144,7 @@ public class InfoListAdapter extends RecyclerView.Adapter offsetStart = " + offsetStart + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter); @@ -140,6 +162,16 @@ public class InfoListAdapter extends RecyclerView.Adapter { + addInfoItem(data, streamStateEntity); + }) + ); + } + + private void addInfoItem(InfoItem data, StreamStateEntity state) { if (data != null) { if (DEBUG) { Log.d(TAG, "addInfoItem() before > infoItemList.size() = " + infoItemList.size() + ", thread = " + Thread.currentThread()); @@ -147,6 +179,7 @@ public class InfoListAdapter extends RecyclerView.Adapter position = " + positionInserted + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter); @@ -167,6 +200,7 @@ public class InfoListAdapter extends RecyclerView.Adapter loadStreamState(final InfoItem info) { + return Single.fromCallable(() -> { + final List entities = streamTable.getStream(info.getServiceId(), info.getUrl()).blockingFirst(); + if (entities.isEmpty()) { + return null; + } + final List states = streamStateTable.getState(entities.get(0).getUid()).blockingFirst(); + if (states.isEmpty()) { + return null; + } + return states.get(0); + }).subscribeOn(Schedulers.io()); + } + + public Single> loadStreamStateBatch(final List infos) { + return Single.fromCallable(() -> { + final List result = new ArrayList<>(infos.size()); + for (InfoItem info : infos) { + final List entities = streamTable.getStream(info.getServiceId(), info.getUrl()).blockingFirst(); + if (entities.isEmpty()) { + result.add(null); + continue; + } + final List states = streamStateTable.getState(entities.get(0).getUid()).blockingFirst(); + if (states.isEmpty()) { + result.add(null); + continue; + } + result.add(states.get(0)); + } + return result; + }).subscribeOn(Schedulers.io()); + } + /////////////////////////////////////////////////////// // Utility /////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java index 7d0fc3e61..364d50df6 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java @@ -23,7 +23,6 @@ import android.support.annotation.Nullable; import android.support.v4.app.FragmentManager; import android.support.v4.content.LocalBroadcastManager; import android.support.v7.app.ActionBar; -import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; @@ -48,10 +47,8 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.info_list.InfoListAdapter; -import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService; import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService; -import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.FilePickerActivityHelper; import org.schabi.newpipe.util.NavigationHelper; @@ -131,6 +128,12 @@ public class SubscriptionFragment extends BaseStateFragment + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_stream_item.xml b/app/src/main/res/layout/list_stream_item.xml index db4af7f8c..ccf325592 100644 --- a/app/src/main/res/layout/list_stream_item.xml +++ b/app/src/main/res/layout/list_stream_item.xml @@ -79,4 +79,16 @@ android:textAppearance="?android:attr/textAppearanceSmall" android:textSize="@dimen/video_item_search_upload_date_text_size" tools:text="2 years ago • 10M views"/> + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_stream_mini_item.xml b/app/src/main/res/layout/list_stream_mini_item.xml index bffd13a6f..383580e59 100644 --- a/app/src/main/res/layout/list_stream_mini_item.xml +++ b/app/src/main/res/layout/list_stream_mini_item.xml @@ -69,4 +69,16 @@ android:textAppearance="?android:attr/textAppearanceSmall" android:textSize="@dimen/video_item_search_uploader_text_size" tools:text="Uploader" /> + + + \ No newline at end of file From a48cbc697151ba4b2aeb20723e1afd7ccc74dc14 Mon Sep 17 00:00:00 2001 From: Vasiliy Date: Mon, 15 Apr 2019 22:18:24 +0300 Subject: [PATCH 070/138] Show streams states for local lists --- .../newpipe/info_list/InfoListAdapter.java | 2 +- .../newpipe/local/BaseLocalListFragment.java | 1 + .../newpipe/local/LocalItemListAdapter.java | 28 +++++++++++++- .../local/dialog/PlaylistAppendDialog.java | 5 ++- .../local/history/HistoryRecordManager.java | 37 +++++++++++++++++-- .../newpipe/local/holder/LocalItemHolder.java | 4 +- .../local/holder/LocalPlaylistItemHolder.java | 6 ++- .../holder/LocalPlaylistStreamItemHolder.java | 15 +++++++- .../LocalStatisticStreamItemHolder.java | 15 +++++++- .../local/holder/PlaylistItemHolder.java | 4 +- .../holder/RemotePlaylistItemHolder.java | 6 ++- .../layout/list_stream_playlist_grid_item.xml | 11 ++++++ .../res/layout/list_stream_playlist_item.xml | 11 ++++++ app/src/main/res/values-land/dimens.xml | 2 +- app/src/main/res/values/dimens.xml | 2 +- 15 files changed, 132 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java index 8e54f582a..df207abb5 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java @@ -166,7 +166,7 @@ public class InfoListAdapter extends RecyclerView.Adapter { - addInfoItem(data, streamStateEntity); + addInfoItem(data, streamStateEntity[0]); }) ); } diff --git a/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java b/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java index abdf82353..20676c6db 100644 --- a/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java @@ -150,6 +150,7 @@ public abstract class BaseLocalListFragment extends BaseStateFragment public void onDestroyView() { super.onDestroyView(); itemsList = null; + itemListAdapter.dispose(); itemListAdapter = null; } diff --git a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java index e298dedd3..372392d7b 100644 --- a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java @@ -8,6 +8,8 @@ import android.view.View; import android.view.ViewGroup; import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; +import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.holder.LocalItemHolder; import org.schabi.newpipe.local.holder.LocalPlaylistGridItemHolder; import org.schabi.newpipe.local.holder.LocalPlaylistItemHolder; @@ -25,6 +27,9 @@ import java.text.DateFormat; import java.util.ArrayList; import java.util.List; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; + /* * Created by Christian Schabesberger on 01.08.16. * @@ -63,7 +68,10 @@ public class LocalItemListAdapter extends RecyclerView.Adapter localItems; + private final ArrayList states; + private final CompositeDisposable stateLoaders; private final DateFormat dateFormat; private boolean showFooter = false; @@ -73,9 +81,12 @@ public class LocalItemListAdapter extends RecyclerView.Adapter(); dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Localization.getPreferredLocale(activity)); + states = new ArrayList<>(); + stateLoaders = new CompositeDisposable(); } public void setSelectedListener(OnClickGesture listener) { @@ -87,6 +98,15 @@ public class LocalItemListAdapter extends RecyclerView.Adapter data) { + stateLoaders.add( + historyRecordManager.loadLocalStreamStateBatch(data) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(streamStateEntities -> + addItems(data, streamStateEntities)) + ); + } + + private void addItems(List data, List streamStates) { if (data != null) { if (DEBUG) { Log.d(TAG, "addItems() before > localItems.size() = " + @@ -95,6 +115,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter offsetStart = " + offsetStart + @@ -130,6 +151,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter= localItems.size() || actualTo >= localItems.size()) return false; localItems.add(actualTo, localItems.remove(actualFrom)); + states.add(actualTo, states.remove(actualFrom)); notifyItemMoved(fromAdapterPosition, toAdapterPosition); return true; } @@ -259,7 +281,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter loadStreamState(final InfoItem info) { + public Single loadStreamState(final InfoItem info) { return Single.fromCallable(() -> { final List entities = streamTable.getStream(info.getServiceId(), info.getUrl()).blockingFirst(); if (entities.isEmpty()) { - return null; + return new StreamStateEntity[]{null}; } final List states = streamStateTable.getState(entities.get(0).getUid()).blockingFirst(); if (states.isEmpty()) { - return null; + return new StreamStateEntity[]{null}; } - return states.get(0); + return new StreamStateEntity[]{states.get(0)}; }).subscribeOn(Schedulers.io()); } @@ -255,6 +258,32 @@ public class HistoryRecordManager { }).subscribeOn(Schedulers.io()); } + public Single> loadLocalStreamStateBatch(final List items) { + return Single.fromCallable(() -> { + final List result = new ArrayList<>(items.size()); + for (LocalItem item : items) { + long streamId; + if (item instanceof StreamStatisticsEntry) { + streamId = ((StreamStatisticsEntry) item).streamId; + } else if (item instanceof PlaylistStreamEntity) { + streamId = ((PlaylistStreamEntity) item).getStreamUid(); + } else if (item instanceof PlaylistStreamEntry) { + streamId = ((PlaylistStreamEntry) item).streamId; + } else { + result.add(null); + continue; + } + final List states = streamStateTable.getState(streamId).blockingFirst(); + if (states.isEmpty()) { + result.add(null); + continue; + } + result.add(states.get(0)); + } + return result; + }).subscribeOn(Schedulers.io()); + } + /////////////////////////////////////////////////////// // Utility /////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalItemHolder.java index 889751afa..01af60f98 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalItemHolder.java @@ -1,10 +1,12 @@ package org.schabi.newpipe.local.holder; +import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.ViewGroup; import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.local.LocalItemBuilder; import java.text.DateFormat; @@ -38,5 +40,5 @@ public abstract class LocalItemHolder extends RecyclerView.ViewHolder { this.itemBuilder = itemBuilder; } - public abstract void updateFromItem(final LocalItem item, final DateFormat dateFormat); + public abstract void updateFromItem(final LocalItem item, @Nullable final StreamStateEntity state, final DateFormat dateFormat); } diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java index 8743684ee..0e6eca9ba 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java @@ -1,10 +1,12 @@ package org.schabi.newpipe.local.holder; +import android.support.annotation.Nullable; import android.view.View; import android.view.ViewGroup; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.local.LocalItemBuilder; import org.schabi.newpipe.util.ImageDisplayConstants; @@ -21,7 +23,7 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder { } @Override - public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) { + public void updateFromItem(final LocalItem localItem, @Nullable final StreamStateEntity state, final DateFormat dateFormat) { if (!(localItem instanceof PlaylistMetadataEntry)) return; final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem; @@ -32,6 +34,6 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder { itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS); - super.updateFromItem(localItem, dateFormat); + super.updateFromItem(localItem, state, dateFormat); } } diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java index e591b73e5..48bbbc81d 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java @@ -1,21 +1,25 @@ package org.schabi.newpipe.local.holder; +import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; +import android.widget.ProgressBar; import android.widget.TextView; import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.local.LocalItemBuilder; import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; import java.text.DateFormat; +import java.util.concurrent.TimeUnit; public class LocalPlaylistStreamItemHolder extends LocalItemHolder { @@ -24,6 +28,7 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder { public final TextView itemAdditionalDetailsView; public final TextView itemDurationView; public final View itemHandleView; + public final ProgressBar itemProgressView; LocalPlaylistStreamItemHolder(LocalItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { super(infoItemBuilder, layoutId, parent); @@ -33,6 +38,7 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder { itemAdditionalDetailsView = itemView.findViewById(R.id.itemAdditionalDetails); itemDurationView = itemView.findViewById(R.id.itemDurationView); itemHandleView = itemView.findViewById(R.id.itemHandle); + itemProgressView = itemView.findViewById(R.id.itemProgressView); } public LocalPlaylistStreamItemHolder(LocalItemBuilder infoItemBuilder, ViewGroup parent) { @@ -40,7 +46,7 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder { } @Override - public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) { + public void updateFromItem(final LocalItem localItem, @Nullable final StreamStateEntity state, final DateFormat dateFormat) { if (!(localItem instanceof PlaylistStreamEntry)) return; final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem; @@ -53,6 +59,13 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder { itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), R.color.duration_background_color)); itemDurationView.setVisibility(View.VISIBLE); + if (state != null) { + itemProgressView.setVisibility(View.VISIBLE); + itemProgressView.setMax((int) item.duration); + itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime())); + } else { + itemProgressView.setVisibility(View.GONE); + } } else { itemDurationView.setVisibility(View.GONE); } diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java index 57a5794e3..ac194b776 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java @@ -5,17 +5,20 @@ import android.support.v4.content.ContextCompat; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; +import android.widget.ProgressBar; import android.widget.TextView; import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.stream.StreamStatisticsEntry; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.local.LocalItemBuilder; import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; import java.text.DateFormat; +import java.util.concurrent.TimeUnit; /* * Created by Christian Schabesberger on 01.08.16. @@ -45,6 +48,7 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { public final TextView itemDurationView; @Nullable public final TextView itemAdditionalDetails; + public final ProgressBar itemProgressView; public LocalStatisticStreamItemHolder(LocalItemBuilder itemBuilder, ViewGroup parent) { this(itemBuilder, R.layout.list_stream_item, parent); @@ -58,6 +62,7 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { itemUploaderView = itemView.findViewById(R.id.itemUploaderView); itemDurationView = itemView.findViewById(R.id.itemDurationView); itemAdditionalDetails = itemView.findViewById(R.id.itemAdditionalDetails); + itemProgressView = itemView.findViewById(R.id.itemProgressView); } private String getStreamInfoDetailLine(final StreamStatisticsEntry entry, @@ -70,7 +75,7 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { } @Override - public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) { + public void updateFromItem(final LocalItem localItem, @Nullable final StreamStateEntity state, final DateFormat dateFormat) { if (!(localItem instanceof StreamStatisticsEntry)) return; final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem; @@ -82,8 +87,16 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), R.color.duration_background_color)); itemDurationView.setVisibility(View.VISIBLE); + if (state != null) { + itemProgressView.setVisibility(View.VISIBLE); + itemProgressView.setMax((int) item.duration); + itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime())); + } else { + itemProgressView.setVisibility(View.GONE); + } } else { itemDurationView.setVisibility(View.GONE); + itemProgressView.setVisibility(View.GONE); } if (itemAdditionalDetails != null) { diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/PlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/PlaylistItemHolder.java index 5d6f192e1..2a81f9571 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/PlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/PlaylistItemHolder.java @@ -1,11 +1,13 @@ package org.schabi.newpipe.local.holder; +import android.support.annotation.Nullable; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.local.LocalItemBuilder; import java.text.DateFormat; @@ -31,7 +33,7 @@ public abstract class PlaylistItemHolder extends LocalItemHolder { } @Override - public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) { + public void updateFromItem(final LocalItem localItem, @Nullable final StreamStateEntity state, final DateFormat dateFormat) { itemView.setOnClickListener(view -> { if (itemBuilder.getOnItemSelectedListener() != null) { itemBuilder.getOnItemSelectedListener().selected(localItem); diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java index 5b2a88d38..bdcd42f67 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java @@ -1,9 +1,11 @@ package org.schabi.newpipe.local.holder; +import android.support.annotation.Nullable; import android.view.ViewGroup; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; +import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.local.LocalItemBuilder; import org.schabi.newpipe.util.ImageDisplayConstants; @@ -21,7 +23,7 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder { } @Override - public void updateFromItem(final LocalItem localItem, final DateFormat dateFormat) { + public void updateFromItem(final LocalItem localItem, @Nullable final StreamStateEntity state, final DateFormat dateFormat) { if (!(localItem instanceof PlaylistRemoteEntity)) return; final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem; @@ -33,6 +35,6 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder { itemBuilder.displayImage(item.getThumbnailUrl(), itemThumbnailView, ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS); - super.updateFromItem(localItem, dateFormat); + super.updateFromItem(localItem, state, dateFormat); } } diff --git a/app/src/main/res/layout/list_stream_playlist_grid_item.xml b/app/src/main/res/layout/list_stream_playlist_grid_item.xml index 4b31a452e..ea680c072 100644 --- a/app/src/main/res/layout/list_stream_playlist_grid_item.xml +++ b/app/src/main/res/layout/list_stream_playlist_grid_item.xml @@ -81,4 +81,15 @@ android:textSize="@dimen/video_item_search_uploader_text_size" tools:text="Uploader" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_stream_playlist_item.xml b/app/src/main/res/layout/list_stream_playlist_item.xml index 193b3fea4..47c3ab93d 100644 --- a/app/src/main/res/layout/list_stream_playlist_item.xml +++ b/app/src/main/res/layout/list_stream_playlist_item.xml @@ -83,4 +83,15 @@ android:textSize="@dimen/video_item_search_uploader_text_size" tools:text="Uploader" /> + + \ No newline at end of file diff --git a/app/src/main/res/values-land/dimens.xml b/app/src/main/res/values-land/dimens.xml index ee047f0da..eea6c5cf0 100644 --- a/app/src/main/res/values-land/dimens.xml +++ b/app/src/main/res/values-land/dimens.xml @@ -14,7 +14,7 @@ 142dp 80dp - 104dp + 106dp 10dp 1sp diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 94101cfb0..582c4bade 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -15,7 +15,7 @@ 164dp 92dp - 94dp + 96dp 12dp 6dp From 1df8af35d4f7134888577faa2033973351421633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristjan=20R=C3=A4ts?= Date: Mon, 15 Apr 2019 18:52:25 +0000 Subject: [PATCH 071/138] Translated using Weblate (Estonian) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-et/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 95065f8e7..b57fdd306 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -103,7 +103,7 @@ Esita Sisu Vanusepiiranguga sisu - Kuva vanusepiiranguga sisu. Sellise materjali saab lubada seadetes. + Kuva vanusepiiranguga video. Sellist sisu saab lubada seadetes. OTSE Allalaadimised Allalaadimised @@ -459,13 +459,13 @@ Serveriga ei saadud ühendust Server ei saada andmeid Server ei toeta mitmelõimelisi allalaadimisi. Proovi uuesti kasutades @string/msg_threads = 1 - Mitterahuldav taotletud vahemik + Taotletud vahemik ei ole rahuldatav Ei leitud Järeltöötlemine nurjus Eemalda lõpetatud allalaadimised Jätka %s pooleliolevat allalaadimist Stopp - Uusi katseid kuni + Korduskatseid Suurim katsete arv enne allalaadimise tühistamist Paus üleminekul mobiilsele andmesidele Allalaadimised, mida ei saa peatada, taaskäivitatakse From a9c64b2fecacb88ea61cb647aa3f1b32b3d23f94 Mon Sep 17 00:00:00 2001 From: YONGLE Date: Mon, 22 Apr 2019 20:01:44 +0000 Subject: [PATCH 072/138] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-cmn/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-cmn/strings.xml b/app/src/main/res/values-cmn/strings.xml index 99ac876a9..858c22b23 100644 --- a/app/src/main/res/values-cmn/strings.xml +++ b/app/src/main/res/values-cmn/strings.xml @@ -470,7 +470,7 @@ NewPipe 更新可用! 无法创建目标文件夹 服务器不接受多线程下载, 请重试使用 @string/msg_threads = 1 - 请求的范围不满足 + 请求范围无法满足 继续进行%s个待下载转移 - 将重新启动无法暂停的下载 + 无法暂停的下载将重新开始 \ No newline at end of file From 08f8b9770af6627568556a56d5572042b8ac53e3 Mon Sep 17 00:00:00 2001 From: yunna Date: Mon, 22 Apr 2019 08:38:27 +0000 Subject: [PATCH 073/138] Translated using Weblate (Japanese) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-ja/strings.xml | 66 +++++++++++++------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index f704b8dbd..dd229db93 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -21,14 +21,14 @@ Koreが見つかりません。Kore を入手しますか? \"Kodi で再生\" オプションを表示 Kodi メディアセンター経由で動画を再生するための設定を表示します - 音楽 - デフォルトの音楽形式 + 音声 + デフォルトの音声形式 保存 「次の動画」と「関連動画」を表示 URLは使用できません 優先言語 - 動画と音楽 + 動画と音声 %1$s ビュー 動画 プレビュー サムネイル 動画 プレビュー サムネイル @@ -40,10 +40,10 @@ バックグラウンドで再生中 再生 Torを使用する - (実験的) 強制的に Tor を経由した経路で保存して、匿名性を高めます(動画の同時再生は未だ対応していません)。 + (実験的) 強制的に Tor を経由した経路で保存して、匿名性を高めます(動画の同時再生には、まだ対応していません)。 テーマ - ダーク - ライト + グレー + ホワイト 外観 その他 ネットワークエラー @@ -70,11 +70,11 @@ 申し訳ありません、不具合が発生しました 報告 情報: - 何が起こりましたか: - あなたの意見(英語で): + 発生した内容: + あなたのコメント(英語で): 詳細: 動画 - 音楽 + 音声 再試行 ストレージへのアクセスが拒否されました 自動再生 @@ -93,7 +93,7 @@ ファイル名 同時接続数 エラー - サーバーが対応していません + このサーバーには対応していません ファイルが既に存在します URL の形式が正しくないか、通信が利用できません NewPipeの保存中 @@ -130,17 +130,17 @@ デフォルトのポップアップ解像度 高い解像度で表示 2K/4K ビデオの再生は一部のデバイスのみサポートしています - 背景 + バックグラウンド ポップアップ フィルター 更新 クリア ポップアップのサイズと位置を記憶する - 前回のポップアップしたサイズと位置を記憶する + 前回ポップアップしたサイズと位置を記憶する ポップアップ サイズを変更 一部の解像度では音声がありません - プレーヤーのジェスチャー コントロール + プレーヤーのジェスチャー制御 ジェスチャーを使用してプレーヤーの明るさと音量をコントロールする 検索候補 検索時に候補を表示します @@ -179,7 +179,7 @@ 動画の詳細ページで背景、またはポップアップボタンが押されたときにヒントを表示する 動作 履歴とキャッシュ - 再生リスト + プレイリスト 元に戻す すべて再生 通知 @@ -191,10 +191,10 @@ 動画がありません 保存 ファイル名に使用可能な文字 - 無効な文字は次の値で置き換えられます - 置換文字 + 無効な文字はここで指定した文字に置き換えられます + ファイル名の自動修正 文字と数字 - ほとんどの特殊文字 + 文字と数字に加え、ほとんどの特殊文字 寄付 NewPipe は、あなたに最高の体験を味わってもらうために、ボランティアが自分たちの時間を使って開発しています。開発者たちがコーヒーを飲みながら NewPipe を継続的に改良できるよう、あなたのご支援をお願いします。 Webサイト @@ -225,16 +225,16 @@ 詳細 音声の設定 画面を回転 - バックグラウンド再生に変更 - ポップアップ再生に変更 - メイン再生に変更 + バックグラウンド再生を開始 + ポップアップ再生を開始 + メイン再生に切り替え 動画プレイヤーが見つかりません (VLCをインストールして再生できます)。 - デフォルトのコンテンツの国 + デフォルトの地域設定 サービス 常に 一度だけ - データベースのインポート - データベースのエクスポート + データベースをインポート + データベースをエクスポート 既存の履歴と購読リストは上書きされます 履歴や購読リスト、プレイリストをエクスポートします 再生エラーからの回復中 @@ -269,8 +269,8 @@ ファイル 動画が見つかりません 音声が見つかりません - そのフォルダーはありません - ファイルが存在しないか、ファイルへの読み書きができません + フォルダーが見つかりません + ファイルが存在しないか、読み書きする権限がありません ファイル名は空白にできません エラーが発生しました: %1$s ダウンロードできる動画はありません @@ -328,11 +328,11 @@ NewPipe プライバシーポリシー プライバシーポリシーを確認 おおまかなシーク - おおまかなシークを使用すると、正確さ下がりますが、高速なシークが可能になります + おおまかなシークを使用すると、正確さが下がりますが高速なシークが可能になります すべてのサムネイルの読み込みと保存を無効化します、このオプションを切り替えるとメモリおよびディスク上の画像キャッシュがクリアされます。 繰り返しでないキューの最後の動画を再生時、関連動画を自動的にキューに追加する。 - すべての再生履歴を削除します? - すべての検索履歴を削除します? + すべての再生履歴を削除しますか? + すべての検索履歴を削除しますか? このファイル/コンテンツはありません 登録者数 %s 人 @@ -351,7 +351,7 @@ 最終再生日時 最も再生した動画 ズーム - 追加... + プレイリスト 「長押しして追加」のヒントを表示 トラック NewPipe バックグラウンドおよびポップアップのプレーヤーの通知 @@ -407,10 +407,10 @@ 更新 催し物 新しい NewPipe バージョンの通知 - 外部記憶装置きません + 外部記憶装置は利用できません 既定値に戻す 既定の設定を復元しますか\? - 加入者数は利用できません + 登録者数は表示できません 選択 会議 ヨーロッパの一般データ保護規制(GDPR)に準拠するために、NewPipeの個人情報保護方針にご注意ください。よく読んでください。 @@ -456,12 +456,12 @@ ダウンロードをキャンセルする前に試行の最大数 音量ジェスチャー制御 ジェスチャーを使用して、プレーヤーの音量を制御します - 明るさジェスチャー制御 + 明るさのジェスチャー制御 ジェスチャーを使用して、プレーヤーの明るさを制御します ファイルを削除しました アプリの更新通知 まだ外部 SD カードにダウンロードできません。ダウンロードフォルダーの場所をリセットしますか\? - デフォルトのタブを使用します。保存タブの読み取りエラーが発生しました + デフォルトのタブを使用します。保存されたタブの読み込みエラーが発生しました メインページに表示されるタブ 新しいバージョンが利用可能なときにアプリの更新を確認する通知を表示します ダウンロードから %s の保留中の転送を続行します From 5314e275bc3209aa837d154fd73e894cadde8e35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Strecke?= Date: Wed, 24 Apr 2019 06:10:14 +0000 Subject: [PATCH 074/138] Translated using Weblate (German) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-de/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 452fd01bf..f83ed7b65 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -410,7 +410,7 @@ Kanäle Wiedergabelisten Titel - Nutzer + Benutzer Deabonnieren Neuer Tab Tab wählen From c300d52b294a8b54feba3663d3fb0b701cc11947 Mon Sep 17 00:00:00 2001 From: Jesper Hertel Date: Wed, 24 Apr 2019 11:08:12 +0000 Subject: [PATCH 075/138] Translated using Weblate (Danish) Currently translated at 87.1% (386 of 443 strings) --- app/src/main/res/values-da/strings.xml | 136 ++++++++++++++----------- 1 file changed, 75 insertions(+), 61 deletions(-) diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index bd1e27af9..6143dc6b0 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -1,16 +1,17 @@ -Tryk søg for at komme i gang + + Tryk søg for at komme i gang %1$s visninger - Udgivet den %1$s + Udgivet %1$s Ingen streamafspiller blev fundet. Vil du installere VLC\? - Ingen streamafspiller blev fundet (du kan installere VLC for at afspille den). + Ingen streamafspiller fundet (du kan installere VLC for at afspille den). Installer Annuller - åben i browser - Åben i popup tilstand + Åbn i browser + Åbn i pop op-tilstand Del Download - Download stream fil + Download stream-fil Søg Indstillinger Mente du: %1$s\? @@ -18,64 +19,64 @@ Vælg browser rotation Benyt ekstern videoafspiller - Fjerner lyd ved nogle opløsninger + Fjerner lyd ved NOGLE opløsninger Brug ekstern lydafspiller - NewPipe popup tilstand + NewPipe pop op-tilstand Abonner Abonneret - Afmeld Abonnement - Abonnement afmeldt for kanal + Afmeld abonnement + Abonnement afmeldt Kunne ikke ændre abonnement - Kunne ikke opdatere abonnoment + Kunne ikke opdatere abonnement Vis info - Forside - Abbonnomenter - Gemte Playlister - Ny Tab - Vælg Tab + Primær + Abonnementer + Gemte spillelister + Ny fane + Vælg fane Nyheder Baggrund - Popup - Tilføj Til - Video download sti - Sti til gemme downloade videoer - Indtast download mappe for videoer - Lyd download mappe + Pop op + Føj til + Placering af videodownloads + Mappe som videoer skal downloades til + Angiv downloadmappe for videoer + Downloadmappe for lydfiler Downloadede lydfiler bliver gemt her - Indtast download mappe for lydfiler + Angiv downloadmappe for lydfiler Afspil automatisk - Afspil en video ny NewPipe bliver åbnet fra en anden app - Standard opløsning - Stadard popup opløsning + Afspiller video automatisk når NewPipe bliver åbnet fra en anden app + Standardopløsning + Standardopløsning for pop op Vis højere opløsninger - Ikke alle enheder understøtter afspilning af 2K/4K videoer + Ikke alle enheder understøtter afspilning af 2K/4K-videoer Afspil med Kodi - Kore appen kunne ikke findes. Installer den\? - Vis \"Afspil med Kodi\" valgmulighed - Vis en knap til at afspille en video via Kode Media Center + Kore-appen ikke fundet. Installer den\? + Vis valgmuligheden \"Afspil med Kodi\" + Vis en knap til at afspille en video via Kodi-mediecenteret Lyd - Standard lydfilformat - Standard videofilformat + Standardformat for lydfiler + Standardformat for videofiler Tema - Lys - Mørk + Lyst + Mørkt Sort - Husk popup størrelse og position - Husk sidste størelse og position for popup afspiller + Husk pop op-størrelse og -position + Husk sidste størrelse og placering af pop op-afspiller Brug hurtig og upræcis søgning - Upræcis søgning gør det hurtigere at søge i afspilningen, men med dårligere præcision - Indlæs billeder - Når slået fra indlæses billeder ikke, derved spares der på netværks og hukommelses forbrug. Ændringer sletter både cachen i ram og på disk. - Billed cache slettet - Slet cached metadata - Slet alt cached webside data - Metadata cache slettet - Tilføj automatisk næste stream til køen - Tilføj relaterede streams til køen automatisk når den sidste stream i køen afspilles. Gælder ikke hvis køen er gentagene. + Upræcis søgning lader afspilleren finde placeringer hurtigere, men mindre præcist + Indlæs miniaturebilleder + Slå fra for at undgå indlæsning af billeder, hvorved der spares data og hukommelse. Ændringer sletter billedcachen i både ram og lager. + Billedcache slettet + Slet metadata-cachen + Slet alle websidedata fra cachen + Metadata-cache slettet + Føj automatisk næste stream til køen + Føj automatisk relaterede streams til køen når den sidste stream i en ikke-repeterende kø afspilles. Juster lydstyrke ved hjælp af fingerkontrol Brug håndbevægelser til at kontrollere lydstyrke - Lysstyrke håndbævegesleskontorl - Burg håndbevægelser til at justere lysstyrke + Styr lysstyrken med fingerbevægelser + Brug fingerbevægelser til at justere afspillerens lysstyrke Afspiller håndbevægelseskontol Brug håndbevægelser til at justere lysstyrke og lydstyrke Søg i foreslag @@ -112,7 +113,7 @@ Afspil Inhold Aldersbegrænset inhold - Vis aldersbegrænset video. Det er er muligt at tillade sådanne video under indstillinger. + Vis aldersbegrænsede videoer. Du kan tillade den slags videoer under Indstillinger. LIVE Downloads Downloads @@ -123,9 +124,9 @@ Playliste Playlister - Videoer - - + Video + Videoer + Numre Brugere Ja @@ -156,8 +157,8 @@ Skift til Forside Importer database Eksporter database - Overskriver din nuværende historie og abebonomenter - Eksporter historie, abebonomenter and playlister + Overskriver din nuværende historie og abonnementer + Eksporter historie, abonnementer og spillelister Slet visningshistorik Sletter historien af viste videoer Slet hele visningshistorikken\? @@ -168,7 +169,7 @@ Søgehistorik slettet. Fejl Ekstern lagring utilgængelig - Download til til eksternt SD kort er ikke muligt i øjeblikket. Genskab download mappe indstilinning\? + Download til eksternt SD-kort er endnu ikke muligt. Nulstil placering af download-mappe\? Netwærksfejl Kunne indlæse alle billeder Kunne ikke dekryptere video URL signatur @@ -221,13 +222,13 @@ Lyd Prøv igen Adgang til hulkomekse nægtet - Ingen abbonenter - Antal af abonnementer er ikke tilgængeligt + Ingen abonnenter + Antallet af abonnenter er ikke tilgængeligt Ingen visninger - %s visning - %s visninger - + %s visning + %s visninger + Ingen Videoer Start Afspil @@ -308,10 +309,10 @@ Udvalg Blank Side Kiosk Side - Abonnement Side + Abonnementsside Kanalside Vælg en kanal - Ingen kanal abebonomenter endnu + Ingen kanalabonnementer endnu Vælg en kiosk Eksporteret Importeret @@ -391,4 +392,17 @@ Ikke fundet Efterbehandling fejlede Stop + Hændelser + Intet at se her + K + mio. + mia. + + %s abonnent + %s abonnenter + + Pause + Feed-side + Kunne ikke importere abonnementer + Kunne ikke eksportere abonnementer \ No newline at end of file From 1d63b395530fb1aef4e77a7ab61662f4e157c886 Mon Sep 17 00:00:00 2001 From: Marc Riera Date: Thu, 25 Apr 2019 14:20:12 +0000 Subject: [PATCH 076/138] Translated using Weblate (Catalan) Currently translated at 91.6% (406 of 443 strings) --- app/src/main/res/values-ca/strings.xml | 83 +++++--------------------- 1 file changed, 14 insertions(+), 69 deletions(-) diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 78608c2a5..9953766e1 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -1,5 +1,6 @@ -Instal·la + + Instal·la Cancel·la Obre al navegador Comparteix @@ -10,12 +11,9 @@ Subscriu-t\'hi Subscrit Mostra la informació - Subscripcions Llistes de reproducció desades - Novetats - Ruta de baixada dels vídeos Carpeta de baixada dels fitxers d\'àudio Reproducció automàtica @@ -54,9 +52,7 @@ Sempre Només una vegada Fitxer - [Desconegut] - Importa una base de dades Exporta una base de dades Exporta l\'historial, les subscripcions i les llistes de reproducció @@ -65,12 +61,10 @@ Vídeo Àudio - %s subscriptor - %s subscriptors - - + %s subscriptor + %s subscriptors + D\'acord - Nom de fitxer Error reCAPTCHA @@ -87,8 +81,6 @@ Lloc web Llicència del NewPipe Llegeix la llicència - - Historial L\'historial està desactivat Historial @@ -111,7 +103,6 @@ Reproductor en segon pla Reproductor emergent Demana-ho sempre - Crea una llista de reproducció Elimina Canvia el nom @@ -120,13 +111,11 @@ Importa Importa des de Exporta a - S\'està important… S\'està exportant… - Importa un fitxer Per defecte -%1$s reproduccions + %1$s reproduccions Publicat el %1$s No s\'ha trobat un reproductor de fluxos. Voleu instal·lar el VLC? No s\'ha trobat cap reproductor de fluxos (podeu instal·lar el VLC per reproduir-lo). @@ -144,13 +133,10 @@ Segon pla Emergent Afegeix a - Ruta on es desaran els vídeos baixats Introduïu una ruta de baixada per als vídeos - Els fitxers d\'àudio baixats es desaran aquí Introduïu una ruta de baixada per als fitxers d\'àudio - Reprodueix un vídeo quan el NewPipe s\'executa des d\'una altra aplicació Resolució per defecte del mode emergent Mostra resolucions superiors @@ -186,7 +172,6 @@ Reprodueix Notificació del NewPipe Notificacions dels reproductors en segon pla o emergents del NewPipe - No s\'han pogut carregar totes les miniatures No s\'ha pogut desencriptar la signatura de l\'URL del vídeo No s\'ha pogut processar el lloc web @@ -216,8 +201,6 @@ Què ha passat: Comentari (en anglès): Detalls: - - Miniatura de previsualització del vídeo Miniatura de previsualització del vídeo Miniatura de l\'avatar del propietari @@ -231,25 +214,21 @@ No hi ha res aquí No s\'ha pogut crear el directori de baixades «%1$s» S\'ha creat el directori de baixades «%1$s» - Torna a intentar-ho S\'ha denegat el permís d\'accés a l\'emmagatzematge Fes servir el reproductor antic Antic reproductor integrat de Mediaframework - Sense subscriptors Sense reproduccions - %s reproducció - %s reproduccions - - + %s reproducció + %s reproduccions + Sense vídeos - %s vídeo - %s vídeos - - + Vídeo + Vídeos + Pausa Reprodueix Crea @@ -259,7 +238,6 @@ Suma de verificació Tanca Canvia el nom - Fils Servidor incompatible El fitxer ja existeix @@ -276,7 +254,6 @@ Per a més informació i notícies, visiteu el nostre lloc web. Última reproducció Més reproduïts - Pàgina d\'un quiosc Pàgina de novetats Pàgina d\'un canal @@ -284,7 +261,6 @@ El fitxer no té un format ZIP vàlid Avís: No s\'han pogut importar tots els fitxers. Això sobreescriurà la configuració actual. - Quiosc Tendències Els millors 50 @@ -297,10 +273,8 @@ Tanca el calaix S\'està obtenint la informació… S\'està carregant el contingut seleccionat - Voleu eliminar aquesta llista de reproducció? No s\'ha pogut eliminar la llista de reproducció. - Importació i exportació Controls de la velocitat de reproducció Tempo @@ -323,31 +297,24 @@ Canvia al mode en segon pla Canvia al mode emergent Canvia al mode principal - Sobreescriu l\'historial i les subscripcions actuals S\'està recuperant el reproductor després de l\'error Ho sentim, això no hauria d\'haver ocorregut. Arrossegueu per a reordenar la llista - mil milions mil milions - Inicia Nova missió L\'URL té un format incorrecte o no hi ha connexió a internet Toqueu aquí per a més detalls Trieu una carpeta de baixades disponible Es necessita aquest permís per a obrir el mode emergent - Camp reCAPTCHA S\'ha sol·licitat l\'emplenament d\'un camp reCAPTCHA - Se substituiran els caràcters no vàlids amb aquest valor Caràcter de substitució - Principals caràcters especials - Ja siguin idees, traduccions, canvis en el disseny, una neteja del codi o canvis importants de programació, la vostra ajuda sempre és benvinguda. Com més feina feta hi hagi, millor! El NewPipe està desenvolupat per voluntaris que fan servir el seu temps lliure per a oferir-vos la millor experiència possible. Feu una aportació per assegurar que els nostres desenvolupadors puguin millorar encara més el NewPipe mentre fan un cafè. Fes la teva aportació @@ -358,33 +325,25 @@ Manteniu premut per afegir a la cua Reprodueix aquí en segon pla Reprodueix aquí en mode emergent - Defineix com a miniatura de la llista de reproducció - Afegeix la llista de reproducció a les adreces d\'interès Elimina l\'adreça d\'interès - S\'ha creat la llista de reproducció S\'ha afegit a la llista de reproducció S\'ha canviat la miniatura de la llista de reproducció. Sense subtítols - Ajusta Omple Escala - Generats automàticament Mida dels subtítols Mida més petita Mida normal Mida més gran - Habilita el LeakCanary Darrera exportació - No s\'han pogut importar les subscripcions No s\'han pogut exportar les subscripcions - Desvincula (pot provocar distorsió) Nightcore Elimina totes les dades de llocs web de la memòria cau @@ -393,16 +352,11 @@ Mostra un missatge d\'ajuda quan el botó de mode en segon pla o emergent estigui premut a la pàgina de detalls d\'un vídeo Què ha passat:\\nPetició:\\nIdioma del contingut:\\nServei:\\nHora GMT:\\nPaquet:\\nVersió:\\nVersió del SO: Aviat hi haurà novetats aquí ;D - - Acció d\'obertura preferida Acció per defecte en obrir continguts — %s - "La supervisió de fugues de memòria pot fer que l\'aplicació deixi de respondre mentre es bolca la memòria " - Informa d\'errors fora del cicle de vida Força l\'informe d\'excepcions Rx que no es puguin transmetre que tinguin lloc fora del cicle de vida d\'un fragment o activitat després de disposar-los - Importeu les vostres subscripcions de YouTube mitjançant el fitxer d\'exportació: \n \n1. Aneu a aquesta URL: %1$s @@ -415,18 +369,13 @@ \n3. Inicieu sessió al vostre compte quan se us demani \n4. Copieu l\'URL de la pàgina on se us redireccioni identificador, soundcloud.com/identificador - Tingueu en compte que això pot comportar un ús intensiu de la xarxa. \n \nVoleu continuar? - No hi ha vídeos que es puguin baixar - Subtítols Modifica la mida del text i el fons dels subtítols. Cal reiniciar l\'aplicació per aplicar els canvis. - No s\'ha trobat cap aplicació que pugui reproduir aquest fitxer - Esborra l\'historial de reproduccions Esborra l\'historial dels vídeos que s\'han reproduït Voleu esborrar tot l\'historial de reproduccions\? @@ -436,10 +385,8 @@ Voleu esborrar tot l\'historial de cerca\? S\'ha esborrat l\'historial de cerca. S\'ha esborrat 1 element. - NewPipe és programari lliure sota llicència copyleft: podeu fer-lo servir, estudiar-lo, compartir-lo i millorar-lo al vostre gust. En concret, podeu redistribuir-lo i/o modificar-lo d\'acord amb els termes de la llicència GNU GPL publicada per la Free Software Foundation, ja sigui la versió 3 o (segons vulgueu) qualsevol altra versió posterior. Voleu importar també la configuració? - Política de privacitat del NewPipe El projecte NewPipe es pren molt seriosament la vostra privacitat. Per aquesta raó, l\'aplicació no emmagatzema cap mena de dades sense el vostre consentiment. \nLa política de privacitat del NewPipe descriu detalladament quines dades s\'envien i s\'emmagatzemen quan envieu un informe d\'error. @@ -448,18 +395,16 @@ \nSi voleu enviar-nos un informe d\'error, l\'haureu d\'acceptar. Accepta Rebutja -Sense restriccions + Sense restriccions Restringeix la resolució quan es facin servir dades mòbils Minimitza en canviar d\'aplicació Acció en canviar a una altra aplicació des del reproductor de vídeo principal — %s Cap Minimitza al reproductor en segon pla Minimitza al reproductor emergent - -Avança ràpid durant el silenci + Avança ràpid durant el silenci Pas Reinicialitza - Canals Llistes de reproducció Pistes From a58af1275c94251797fdc03d7a2fce2b90842227 Mon Sep 17 00:00:00 2001 From: pjammo Date: Thu, 25 Apr 2019 16:46:37 +0000 Subject: [PATCH 077/138] Translated using Weblate (Italian) Currently translated at 99.5% (441 of 443 strings) --- app/src/main/res/values-it/strings.xml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index b53e82d54..ad9021f75 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -2,17 +2,17 @@ %1$s visualizzazioni Pubblicato il %1$s - Nessun lettore multimediale trovato. Vuoi installare VLC? + Nessun lettore multimediale trovato. Vuoi installare VLC\? Installa Annulla - Apri nel browser + Apri nel Browser Condividi Scarica Cerca Impostazioni Intendevi: %1$s\? Condividi con - Scegli il browser + Scegli Browser rotazione Percorso dei video scaricati Cartella in cui salvare i video scaricati @@ -37,8 +37,8 @@ Mi piace Impossibile creare la cartella di download \'%1$s\' Creata la cartella per i download \'%1$s\' - Usa un lettore video esterno - Usa un lettore audio esterno + Usa Lettore Video Esterno + Usa Lettore Audio Esterno Cartella degli audio scaricati Cartella in cui salvare gli audio scaricati Inserisci la cartella per gli audio scaricati @@ -118,8 +118,8 @@ È richiesta la risoluzione del reCAPTCHA Più tardi - Apri in modalità popup - Modalità popup di NewPipe + Apri in Modalità Popup + Modalità Popup di NewPipe Riproduzione in modalità popup Disattivato Usa il vecchio lettore multimediale @@ -163,7 +163,7 @@ Informazioni su NewPipe Iscriviti Iscritto - Disiscritto dal canale + Disiscritto dal Canale Impossibile cambiare l\'iscrizione Impossibile aggiornare l\'iscrizione Iscrizioni From c47d4fd35ad07ed67622e129da8c2b35c70511b2 Mon Sep 17 00:00:00 2001 From: Chandra Mohan Jha Date: Sat, 27 Apr 2019 07:55:18 +0000 Subject: [PATCH 078/138] Translated using Weblate (Hindi) Currently translated at 74.5% (330 of 443 strings) --- app/src/main/res/values-hi/strings.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 06f53dcd6..7e8400456 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -6,7 +6,7 @@ इंस्टॉल करें ब्राउज़र में खोलें पॉपअप मोड में खोलें - शेयर करें + साझा करें डाउनलोड खोजें सेटिंग्स @@ -23,7 +23,7 @@ ऑडियो हलका काली - पूर्व और कैश + इतिहास व कॅशे डाउनलोड अगला वीडियो और ऑडियो @@ -92,9 +92,9 @@ जब कुछ ढूंड रहे हो तो सुझाव दिखाये खोज के इतिहास को देखे खोज के query को फ़ोन की मेमोरी में ही रखे - देखे हुए वीडियोस का रिकॉर्ड रखें - जब फोकस मिले तो विडियो resume हो - रूकावट आने पर भी विडियो को जारी रखे (जैसे - फ़ोन कॉल आये) + देखे हुए विडियो की सूची रखे + फोकस मिलने पर विडियो पुनः आगे चले + रूकावट आने पर भी विडियो को जारी रखें (जैसे - फ़ोन कॉल आये) \'अगला\' और \'पहले समान\' वीडियो दिखाए \"जोड़ने के लिए पकड़ें रहे\" दिखाए जब बैकग्राउंड और पॉपअप बटन विडियो के विवरण पन्ने में दबाई जाए तो tip को दिखाए From 8b1836d6999632600fa9bacb47e4cfe8688b879b Mon Sep 17 00:00:00 2001 From: sXp Date: Sat, 27 Apr 2019 07:55:35 +0000 Subject: [PATCH 079/138] Translated using Weblate (Hindi) Currently translated at 74.5% (330 of 443 strings) --- app/src/main/res/values-hi/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 7e8400456..f9137430a 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -60,8 +60,8 @@ कुछ रेसोल्युशनस में नहीं आएगी अन्य ऑडियो प्लेयर उपयोग करें मुख्य - "सदस्यता को बदलने में असमर्थ " - सदस्यता को अद्यतित नहीं किया जा सका + सदस्यता नहीं बदला जा सका + सदस्यता का नवीनीकरण नहीं हो सका देखे की क्या नया है विडियो को डाउनलोड करने के लिए फाइल की जगह डाउनलोड किए गए विडियो फाइल को रखने की जगह From 4a54fbb8729fd450ee0392805007dfe86af18f12 Mon Sep 17 00:00:00 2001 From: Chandra Mohan Jha Date: Sat, 27 Apr 2019 07:58:08 +0000 Subject: [PATCH 080/138] Translated using Weblate (Hindi) Currently translated at 74.5% (330 of 443 strings) --- app/src/main/res/values-hi/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index f9137430a..9755c26fb 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -61,7 +61,7 @@ अन्य ऑडियो प्लेयर उपयोग करें मुख्य सदस्यता नहीं बदला जा सका - सदस्यता का नवीनीकरण नहीं हो सका + सदस्यता का अद्यतन नहीं हो सका देखे की क्या नया है विडियो को डाउनलोड करने के लिए फाइल की जगह डाउनलोड किए गए विडियो फाइल को रखने की जगह From aadbfe1eed453a0d43197d797989a90ae72fffea Mon Sep 17 00:00:00 2001 From: sXp Date: Sat, 27 Apr 2019 07:58:26 +0000 Subject: [PATCH 081/138] Translated using Weblate (Hindi) Currently translated at 74.5% (330 of 443 strings) --- app/src/main/res/values-hi/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 9755c26fb..b6f63e624 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -6,7 +6,7 @@ इंस्टॉल करें ब्राउज़र में खोलें पॉपअप मोड में खोलें - साझा करें + बाँटे डाउनलोड खोजें सेटिंग्स From e7d0685ebc9c097970e6fdf0905e1de7df39572a Mon Sep 17 00:00:00 2001 From: Chandra Mohan Jha Date: Sat, 27 Apr 2019 07:58:31 +0000 Subject: [PATCH 082/138] Translated using Weblate (Hindi) Currently translated at 74.5% (330 of 443 strings) --- app/src/main/res/values-hi/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index b6f63e624..9755c26fb 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -6,7 +6,7 @@ इंस्टॉल करें ब्राउज़र में खोलें पॉपअप मोड में खोलें - बाँटे + साझा करें डाउनलोड खोजें सेटिंग्स From 41fb6f54642c04ff450a91a051f852efeeaf947a Mon Sep 17 00:00:00 2001 From: Vasiliy Date: Sat, 27 Apr 2019 18:01:18 +0300 Subject: [PATCH 083/138] Update states in lists --- .../stream/model/StreamStateEntity.java | 9 +++++++ .../fragments/list/BaseListFragment.java | 2 ++ .../newpipe/info_list/InfoListAdapter.java | 24 +++++++++++++++++++ .../newpipe/local/BaseLocalListFragment.java | 1 + .../newpipe/local/LocalItemListAdapter.java | 22 +++++++++++++++++ 5 files changed, 58 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java index 946ee1182..d46d5cd74 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java @@ -4,6 +4,7 @@ package org.schabi.newpipe.database.stream.model; import android.arch.persistence.room.ColumnInfo; import android.arch.persistence.room.Entity; import android.arch.persistence.room.ForeignKey; +import android.support.annotation.Nullable; import java.util.concurrent.TimeUnit; @@ -62,4 +63,12 @@ public class StreamStateEntity { return seconds > PLAYBACK_SAVE_THRESHOLD_START_SECONDS && seconds < durationInSeconds - PLAYBACK_SAVE_THRESHOLD_END_SECONDS; } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj instanceof StreamStateEntity) { + return ((StreamStateEntity) obj).streamUid == streamUid + && ((StreamStateEntity) obj).progressTime == progressTime; + } else return false; + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java index 5a49cce28..53d549a46 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -100,6 +100,8 @@ public abstract class BaseListFragment extends BaseStateFragment implem } updateFlags = 0; } + + infoListAdapter.updateStates(); } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java index df207abb5..8a30c998a 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java @@ -31,6 +31,7 @@ import org.schabi.newpipe.util.OnClickGesture; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; @@ -195,6 +196,29 @@ public class InfoListAdapter extends RecyclerView.Adapter { + if (streamStateEntities.size() == states.size()) { + for (int i = 0; i < states.size(); i++) { + final StreamStateEntity newState = streamStateEntities.get(i); + if (!Objects.equals(states.get(i), newState)) { + states.set(i, newState); + notifyItemChanged(header == null ? i : i + 1); + } + } + } else { + //oops, something is wrong + } + }) + ); + } + public void clearStreamItemList() { if (infoItemList.isEmpty()) { return; diff --git a/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java b/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java index 20676c6db..75d49e466 100644 --- a/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java @@ -76,6 +76,7 @@ public abstract class BaseLocalListFragment extends BaseStateFragment } updateFlags = 0; } + itemListAdapter.updateStates(); } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java index 372392d7b..80d008231 100644 --- a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java @@ -26,6 +26,7 @@ import org.schabi.newpipe.util.OnClickGesture; import java.text.DateFormat; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; @@ -136,6 +137,27 @@ public class LocalItemListAdapter extends RecyclerView.Adapter { + if (streamStateEntities.size() == states.size()) { + for (int i = 0; i < states.size(); i++) { + final StreamStateEntity newState = streamStateEntities.get(i); + if (!Objects.equals(states.get(i), newState)) { + states.set(i, newState); + notifyItemChanged(header == null ? i : i + 1); + } + } + } else { + //oops, something is wrong + } + }) + ); + } + public void removeItem(final LocalItem data) { final int index = localItems.indexOf(data); From c7cd9e86ac41b08ca742bb583dfb197275e406de Mon Sep 17 00:00:00 2001 From: Vasiliy Date: Sat, 27 Apr 2019 19:04:13 +0300 Subject: [PATCH 084/138] Option to disable states indicators --- .../newpipe/info_list/InfoListAdapter.java | 82 ++++++++++++------- .../newpipe/local/LocalItemListAdapter.java | 32 ++++++-- app/src/main/res/values-ru/strings.xml | 2 + app/src/main/res/values-uk/strings.xml | 2 + app/src/main/res/values/settings_keys.xml | 1 + app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/history_settings.xml | 8 ++ 7 files changed, 93 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java index 8a30c998a..e74a6f2ba 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java @@ -1,12 +1,16 @@ package org.schabi.newpipe.info_list; import android.app.Activity; +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.View; import android.view.ViewGroup; +import org.schabi.newpipe.R; import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; @@ -128,13 +132,19 @@ public class InfoListAdapter extends RecyclerView.Adapter data) { - stateLoaders.add( - historyRecordManager.loadStreamStateBatch(data) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(streamStateEntities -> { - addInfoItemList(data, streamStateEntities); - }) - ); + if (isPlaybackStatesVisible()) { + stateLoaders.add( + historyRecordManager.loadStreamStateBatch(data) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(streamStateEntities -> { + addInfoItemList(data, streamStateEntities); + }) + ); + } else { + final ArrayList states = new ArrayList<>(data.size()); + for (int i = data.size(); i > 0; i--) states.add(null); + addInfoItemList(data, states); + } } private void addInfoItemList(List data, List statesEntities) { @@ -163,13 +173,17 @@ public class InfoListAdapter extends RecyclerView.Adapter { - addInfoItem(data, streamStateEntity[0]); - }) - ); + if (isPlaybackStatesVisible()) { + stateLoaders.add( + historyRecordManager.loadStreamState(data) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(streamStateEntity -> { + addInfoItem(data, streamStateEntity[0]); + }) + ); + } else { + addInfoItem(data, null); + } } private void addInfoItem(InfoItem data, StreamStateEntity state) { @@ -200,23 +214,25 @@ public class InfoListAdapter extends RecyclerView.Adapter { - if (streamStateEntities.size() == states.size()) { - for (int i = 0; i < states.size(); i++) { - final StreamStateEntity newState = streamStateEntities.get(i); - if (!Objects.equals(states.get(i), newState)) { - states.set(i, newState); - notifyItemChanged(header == null ? i : i + 1); + if (isPlaybackStatesVisible()) { + stateLoaders.add( + historyRecordManager.loadStreamStateBatch(infoItemList) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe((streamStateEntities) -> { + if (streamStateEntities.size() == states.size()) { + for (int i = 0; i < states.size(); i++) { + final StreamStateEntity newState = streamStateEntities.get(i); + if (!Objects.equals(states.get(i), newState)) { + states.set(i, newState); + notifyItemChanged(header == null ? i : i + 1); + } } + } else { + //oops, something is wrong } - } else { - //oops, something is wrong - } - }) - ); + }) + ); + } } public void clearStreamItemList() { @@ -363,4 +379,12 @@ public class InfoListAdapter extends RecyclerView.Adapter data) { - stateLoaders.add( - historyRecordManager.loadLocalStreamStateBatch(data) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(streamStateEntities -> - addItems(data, streamStateEntities)) - ); + if (isPlaybackStatesVisible()) { + stateLoaders.add( + historyRecordManager.loadLocalStreamStateBatch(data) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(streamStateEntities -> + addItems(data, streamStateEntities)) + ); + } else { + final ArrayList states = new ArrayList<>(data.size()); + for (int i = data.size(); i > 0; i--) states.add(null); + addItems(data, states); + } } private void addItems(List data, List streamStates) { @@ -138,7 +148,7 @@ public class LocalItemListAdapter extends RecyclerView.AdapterИстория просмотров Продолжать воспроизведение Восстанавливать с последней позиции + Позиции в списках + Отображать индикаторы позиций просмотра в списках Очистить данные Запоминать воспроизведённые потоки Возобновить при фокусе diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 41e6f22ba..0ab0ba78d 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -156,6 +156,8 @@ Історія переглядiв Продовживати перегляд Відновлювати останню позицію + Позиції у списках + Відображати індикатори позицій переглядів у списках Очистити дані Вести облік перегляду відеозаписів Відновити відтворення diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index b70305d56..dd9c30080 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -151,6 +151,7 @@ enable_watch_history main_page_content enable_playback_resume + enable_playback_state_lists import_data export_data diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4f2ef075c..d92dbeb09 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -98,6 +98,8 @@ Watch history Resume playback Restore last playback position + Positions in lists + Show playback position indicators in lists Clear data Keep track of watched videos Resume on focus gain diff --git a/app/src/main/res/xml/history_settings.xml b/app/src/main/res/xml/history_settings.xml index 305b1c360..cae2d56c0 100644 --- a/app/src/main/res/xml/history_settings.xml +++ b/app/src/main/res/xml/history_settings.xml @@ -19,6 +19,14 @@ android:title="@string/enable_playback_resume_title" app:iconSpaceReserved="false" /> + + Date: Sat, 27 Apr 2019 21:23:52 +0300 Subject: [PATCH 085/138] Refactor adapter --- .../newpipe/info_list/InfoListAdapter.java | 173 ++++++---------- .../info_list/StateObjectsListAdapter.java | 189 ++++++++++++++++++ .../newpipe/local/LocalItemListAdapter.java | 122 ++++------- .../schabi/newpipe/util/SparseArrayUtils.java | 30 +++ 4 files changed, 314 insertions(+), 200 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/info_list/StateObjectsListAdapter.java create mode 100644 app/src/main/java/org/schabi/newpipe/util/SparseArrayUtils.java diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java index e74a6f2ba..3cd06f3d6 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java @@ -1,16 +1,14 @@ package org.schabi.newpipe.info_list; import android.app.Activity; -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.View; import android.view.ViewGroup; -import org.schabi.newpipe.R; import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; @@ -29,16 +27,11 @@ import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamGridInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder; import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder; -import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.util.FallbackViewHolder; import org.schabi.newpipe.util.OnClickGesture; import java.util.ArrayList; import java.util.List; -import java.util.Objects; - -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; /* * Created by Christian Schabesberger on 01.08.16. @@ -60,7 +53,7 @@ import io.reactivex.disposables.CompositeDisposable; * along with NewPipe. If not, see . */ -public class InfoListAdapter extends RecyclerView.Adapter { +public class InfoListAdapter extends StateObjectsListAdapter { private static final String TAG = InfoListAdapter.class.getSimpleName(); private static final boolean DEBUG = false; @@ -80,10 +73,7 @@ public class InfoListAdapter extends RecyclerView.Adapter infoItemList; - private final ArrayList states; - private final CompositeDisposable stateLoaders; private boolean useMiniVariant = false; private boolean useGridVariant = false; private boolean showFooter = false; @@ -100,11 +90,9 @@ public class InfoListAdapter extends RecyclerView.Adapter(); - states = new ArrayList<>(); - stateLoaders = new CompositeDisposable(); } public void setOnStreamSelectedListener(OnClickGesture listener) { @@ -131,107 +119,64 @@ public class InfoListAdapter extends RecyclerView.Adapter data) { - if (isPlaybackStatesVisible()) { - stateLoaders.add( - historyRecordManager.loadStreamStateBatch(data) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(streamStateEntities -> { - addInfoItemList(data, streamStateEntities); - }) - ); - } else { - final ArrayList states = new ArrayList<>(data.size()); - for (int i = data.size(); i > 0; i--) states.add(null); - addInfoItemList(data, states); - } - } - - private void addInfoItemList(List data, List statesEntities) { + public void addInfoItemList(@Nullable final List data) { if (data != null) { - if (DEBUG) { - Log.d(TAG, "addInfoItemList() before > infoItemList.size() = " + infoItemList.size() + ", data.size() = " + data.size()); - } - - int offsetStart = sizeConsideringHeaderOffset(); - infoItemList.addAll(data); - states.addAll(statesEntities); - - if (DEBUG) { - Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter); - } - - notifyItemRangeInserted(offsetStart, data.size()); - - if (footer != null && showFooter) { - int footerNow = sizeConsideringHeaderOffset(); - notifyItemMoved(offsetStart, footerNow); - - if (DEBUG) Log.d(TAG, "addInfoItemList() footer from " + offsetStart + " to " + footerNow); - } + loadStates(data, infoItemList.size(), () -> addInfoItemListImpl(data)); } } - public void addInfoItem(InfoItem data) { - if (isPlaybackStatesVisible()) { - stateLoaders.add( - historyRecordManager.loadStreamState(data) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(streamStateEntity -> { - addInfoItem(data, streamStateEntity[0]); - }) - ); - } else { - addInfoItem(data, null); + private void addInfoItemListImpl(@NonNull List data) { + if (DEBUG) { + Log.d(TAG, "addInfoItemList() before > infoItemList.size() = " + infoItemList.size() + ", data.size() = " + data.size()); + } + + int offsetStart = sizeConsideringHeaderOffset(); + infoItemList.addAll(data); + + if (DEBUG) { + Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter); + } + + notifyItemRangeInserted(offsetStart, data.size()); + + if (footer != null && showFooter) { + int footerNow = sizeConsideringHeaderOffset(); + notifyItemMoved(offsetStart, footerNow); + + if (DEBUG) Log.d(TAG, "addInfoItemList() footer from " + offsetStart + " to " + footerNow); } } - private void addInfoItem(InfoItem data, StreamStateEntity state) { + public void addInfoItem(@Nullable InfoItem data) { if (data != null) { - if (DEBUG) { - Log.d(TAG, "addInfoItem() before > infoItemList.size() = " + infoItemList.size() + ", thread = " + Thread.currentThread()); - } + loadState(data, infoItemList.size(), () -> addInfoItemImpl(data)); + } + } - int positionInserted = sizeConsideringHeaderOffset(); - infoItemList.add(data); - states.add(state); + private void addInfoItemImpl(@NonNull InfoItem data) { + if (DEBUG) { + Log.d(TAG, "addInfoItem() before > infoItemList.size() = " + infoItemList.size() + ", thread = " + Thread.currentThread()); + } - if (DEBUG) { - Log.d(TAG, "addInfoItem() after > position = " + positionInserted + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter); - } - notifyItemInserted(positionInserted); + int positionInserted = sizeConsideringHeaderOffset(); + infoItemList.add(data); - if (footer != null && showFooter) { - int footerNow = sizeConsideringHeaderOffset(); - notifyItemMoved(positionInserted, footerNow); + if (DEBUG) { + Log.d(TAG, "addInfoItem() after > position = " + positionInserted + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter); + } + notifyItemInserted(positionInserted); - if (DEBUG) Log.d(TAG, "addInfoItem() footer from " + positionInserted + " to " + footerNow); - } + if (footer != null && showFooter) { + int footerNow = sizeConsideringHeaderOffset(); + notifyItemMoved(positionInserted, footerNow); + + if (DEBUG) Log.d(TAG, "addInfoItem() footer from " + positionInserted + " to " + footerNow); } } public void updateStates() { - if (infoItemList.isEmpty()) { - return; - } - if (isPlaybackStatesVisible()) { - stateLoaders.add( - historyRecordManager.loadStreamStateBatch(infoItemList) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe((streamStateEntities) -> { - if (streamStateEntities.size() == states.size()) { - for (int i = 0; i < states.size(); i++) { - final StreamStateEntity newState = streamStateEntities.get(i); - if (!Objects.equals(states.get(i), newState)) { - states.set(i, newState); - notifyItemChanged(header == null ? i : i + 1); - } - } - } else { - //oops, something is wrong - } - }) - ); + if (!infoItemList.isEmpty()) { + updateAllStates(infoItemList); } } @@ -240,7 +185,7 @@ public class InfoListAdapter extends RecyclerView.Adapter { + + private final SparseArray states; + private final HistoryRecordManager recordManager; + private final CompositeDisposable stateLoaders; + private final Context context; + + public StateObjectsListAdapter(Context context) { + this.states = new SparseArray<>(); + this.recordManager = new HistoryRecordManager(context); + this.context = context; + this.stateLoaders = new CompositeDisposable(); + } + + @Nullable + public StreamStateEntity getState(int position) { + return states.get(position); + } + + protected void clearStates() { + states.clear(); + } + + private void appendStates(List statesEntities, int offset) { + for (int i = 0; i < statesEntities.size(); i++) { + final StreamStateEntity state = statesEntities.get(i); + if (state != null) { + states.append(offset + i, state); + } + } + } + + private void appendState(StreamStateEntity statesEntity, int offset) { + if (statesEntity != null) { + states.append(offset, statesEntity); + } + } + + protected void removeState(int index) { + states.remove(index); + } + + protected void moveState(int from, int to) { + final StreamStateEntity item = states.get(from); + if (from < to) { + SparseArrayUtils.shiftItemsDown(states, from, to); + } else { + SparseArrayUtils.shiftItemsUp(states, to, from); + } + states.put(to, item); + } + + protected void loadStates(List list, int offset, Runnable callback) { + if (isPlaybackStatesVisible()) { + stateLoaders.add( + recordManager.loadStreamStateBatch(list) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(streamStateEntities -> { + appendStates(streamStateEntities, offset); + callback.run(); + }, throwable -> { + if (BuildConfig.DEBUG) throwable.printStackTrace(); + callback.run(); + }) + ); + } else { + callback.run(); + } + } + + protected void loadState(InfoItem item, int offset, Runnable callback) { + if (isPlaybackStatesVisible()) { + stateLoaders.add( + recordManager.loadStreamState(item) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(streamStateEntities -> { + appendState(streamStateEntities[0], offset); + callback.run(); + }, throwable -> { + if (BuildConfig.DEBUG) throwable.printStackTrace(); + callback.run(); + }) + ); + } else { + callback.run(); + } + } + + protected void loadStatesForLocal(List list, int offset, Runnable callback) { + if (isPlaybackStatesVisible()) { + stateLoaders.add( + recordManager.loadLocalStreamStateBatch(list) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(streamStateEntities -> { + appendStates(streamStateEntities, offset); + callback.run(); + }, throwable -> { + if (BuildConfig.DEBUG) throwable.printStackTrace(); + callback.run(); + }) + ); + } else { + callback.run(); + } + } + + private void processStatesUpdates(List streamStateEntities) { + for (int i = 0; i < streamStateEntities.size(); i++) { + final StreamStateEntity newState = streamStateEntities.get(i); + if (!Objects.equals(states.get(i), newState)) { + if (newState == null) { + states.remove(i); + } else { + states.put(i, newState); + } + onItemStateChanged(i, newState); + } + } + } + + protected void updateAllStates(List list) { + if (isPlaybackStatesVisible()) { + stateLoaders.add( + recordManager.loadStreamStateBatch(list) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::processStatesUpdates, throwable -> { + if (BuildConfig.DEBUG) throwable.printStackTrace(); + }) + ); + } else { + final int[] positions = SparseArrayUtils.getKeys(states); + states.clear(); + for (int pos : positions) onItemStateChanged(pos, null); + } + } + + protected void updateAllLocalStates(List list) { + if (isPlaybackStatesVisible()) { + stateLoaders.add( + recordManager.loadLocalStreamStateBatch(list) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::processStatesUpdates, throwable -> { + if (BuildConfig.DEBUG) throwable.printStackTrace(); + }) + ); + } else { + final int[] positions = SparseArrayUtils.getKeys(states); + states.clear(); + for (int pos : positions) onItemStateChanged(pos, null); + } + } + + public void dispose() { + stateLoaders.dispose(); + } + + protected boolean isPlaybackStatesVisible() { + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + return prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true) + && prefs.getBoolean(context.getString(R.string.enable_playback_resume_key), true) + && prefs.getBoolean(context.getString(R.string.enable_playback_state_lists_key), true); + } + + protected abstract void onItemStateChanged(int position, @Nullable StreamStateEntity state); + +} diff --git a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java index 6dddcb714..903712af2 100644 --- a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java @@ -1,19 +1,17 @@ package org.schabi.newpipe.local; import android.app.Activity; -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.View; import android.view.ViewGroup; -import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.stream.model.StreamStateEntity; -import org.schabi.newpipe.local.history.HistoryRecordManager; +import org.schabi.newpipe.info_list.StateObjectsListAdapter; import org.schabi.newpipe.local.holder.LocalItemHolder; import org.schabi.newpipe.local.holder.LocalPlaylistGridItemHolder; import org.schabi.newpipe.local.holder.LocalPlaylistItemHolder; @@ -30,10 +28,6 @@ import org.schabi.newpipe.util.OnClickGesture; import java.text.DateFormat; import java.util.ArrayList; import java.util.List; -import java.util.Objects; - -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; /* * Created by Christian Schabesberger on 01.08.16. @@ -55,7 +49,7 @@ import io.reactivex.disposables.CompositeDisposable; * along with NewPipe. If not, see . */ -public class LocalItemListAdapter extends RecyclerView.Adapter { +public class LocalItemListAdapter extends StateObjectsListAdapter { private static final String TAG = LocalItemListAdapter.class.getSimpleName(); private static final boolean DEBUG = false; @@ -73,10 +67,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter localItems; - private final ArrayList states; - private final CompositeDisposable stateLoaders; private final DateFormat dateFormat; private boolean showFooter = false; @@ -85,13 +76,11 @@ public class LocalItemListAdapter extends RecyclerView.Adapter(); dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Localization.getPreferredLocale(activity)); - states = new ArrayList<>(); - stateLoaders = new CompositeDisposable(); } public void setSelectedListener(OnClickGesture listener) { @@ -102,76 +91,49 @@ public class LocalItemListAdapter extends RecyclerView.Adapter data) { - if (isPlaybackStatesVisible()) { - stateLoaders.add( - historyRecordManager.loadLocalStreamStateBatch(data) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(streamStateEntities -> - addItems(data, streamStateEntities)) - ); - } else { - final ArrayList states = new ArrayList<>(data.size()); - for (int i = data.size(); i > 0; i--) states.add(null); - addItems(data, states); + public void addItems(@Nullable List data) { + if (data != null) { + loadStatesForLocal(data, localItems.size(), () -> addItemsImpl(data)); } } - private void addItems(List data, List streamStates) { - if (data != null) { - if (DEBUG) { - Log.d(TAG, "addItems() before > localItems.size() = " + - localItems.size() + ", data.size() = " + data.size()); - } + private void addItemsImpl(@NonNull List data) { + if (DEBUG) { + Log.d(TAG, "addItems() before > localItems.size() = " + + localItems.size() + ", data.size() = " + data.size()); + } - int offsetStart = sizeConsideringHeader(); - localItems.addAll(data); - states.addAll(streamStates); + int offsetStart = sizeConsideringHeader(); + localItems.addAll(data); - if (DEBUG) { - Log.d(TAG, "addItems() after > offsetStart = " + offsetStart + - ", localItems.size() = " + localItems.size() + - ", header = " + header + ", footer = " + footer + - ", showFooter = " + showFooter); - } + if (DEBUG) { + Log.d(TAG, "addItems() after > offsetStart = " + offsetStart + + ", localItems.size() = " + localItems.size() + + ", header = " + header + ", footer = " + footer + + ", showFooter = " + showFooter); + } - notifyItemRangeInserted(offsetStart, data.size()); + notifyItemRangeInserted(offsetStart, data.size()); - if (footer != null && showFooter) { - int footerNow = sizeConsideringHeader(); - notifyItemMoved(offsetStart, footerNow); + if (footer != null && showFooter) { + int footerNow = sizeConsideringHeader(); + notifyItemMoved(offsetStart, footerNow); - if (DEBUG) Log.d(TAG, "addItems() footer from " + offsetStart + - " to " + footerNow); - } + if (DEBUG) Log.d(TAG, "addItems() footer from " + offsetStart + + " to " + footerNow); } } public void updateStates() { - if (localItems.isEmpty() || !isPlaybackStatesVisible()) return; - stateLoaders.add( - historyRecordManager.loadLocalStreamStateBatch(localItems) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe((streamStateEntities) -> { - if (streamStateEntities.size() == states.size()) { - for (int i = 0; i < states.size(); i++) { - final StreamStateEntity newState = streamStateEntities.get(i); - if (!Objects.equals(states.get(i), newState)) { - states.set(i, newState); - notifyItemChanged(header == null ? i : i + 1); - } - } - } else { - //oops, something is wrong - } - }) - ); + if (!localItems.isEmpty()) { + updateAllLocalStates(localItems); + } } public void removeItem(final LocalItem data) { final int index = localItems.indexOf(data); - localItems.remove(index); + removeState(index); notifyItemRemoved(index + (header != null ? 1 : 0)); } @@ -183,7 +145,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter= localItems.size() || actualTo >= localItems.size()) return false; localItems.add(actualTo, localItems.remove(actualFrom)); - states.add(actualTo, states.remove(actualFrom)); + moveState(actualFrom, actualTo); notifyItemMoved(fromAdapterPosition, toAdapterPosition); return true; } @@ -193,6 +155,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter void shiftItemsDown(SparseArray sparseArray, int lower, int upper) { + for (int i = lower + 1; i <= upper; i++) { + final T o = sparseArray.get(i); + sparseArray.put(i - 1, o); + sparseArray.remove(i); + } + } + + public static void shiftItemsUp(SparseArray sparseArray, int lower, int upper) { + for (int i = upper - 1; i >= lower; i--) { + final T o = sparseArray.get(i); + sparseArray.put(i + 1, o); + sparseArray.remove(i); + } + } + + public static int[] getKeys(SparseArray sparseArray) { + final int[] result = new int[sparseArray.size()]; + for (int i = 0; i < result.length; i++) { + result[i] = sparseArray.keyAt(i); + } + return result; + } +} From 93f25181596792ed35287cd12fff55d97aa6ffe0 Mon Sep 17 00:00:00 2001 From: Vasiliy Date: Sat, 27 Apr 2019 22:27:08 +0300 Subject: [PATCH 086/138] Animate states changed --- .../fragments/detail/VideoDetailFragment.java | 2 +- .../fragments/list/BaseListFragment.java | 2 +- .../newpipe/info_list/InfoListAdapter.java | 19 +++++++++++++++- .../info_list/holder/InfoItemHolder.java | 3 +++ .../holder/StreamMiniInfoItemHolder.java | 21 ++++++++++++++++-- .../newpipe/local/BaseLocalListFragment.java | 3 ++- .../newpipe/local/LocalItemListAdapter.java | 19 +++++++++++++++- .../newpipe/local/holder/LocalItemHolder.java | 3 +++ .../holder/LocalPlaylistStreamItemHolder.java | 22 +++++++++++++++++-- .../LocalStatisticStreamItemHolder.java | 22 +++++++++++++++++-- .../subscription/SubscriptionFragment.java | 2 ++ .../newpipe/views/AnimatedProgressBar.java | 9 ++------ .../main/res/layout/list_stream_grid_item.xml | 2 +- app/src/main/res/layout/list_stream_item.xml | 2 +- .../main/res/layout/list_stream_mini_item.xml | 2 +- .../res/layout/list_stream_playlist_item.xml | 2 +- 16 files changed, 113 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index d6630c9c3..42ac62c59 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -1287,7 +1287,7 @@ public class VideoDetailFragment .subscribe(state -> { final int seconds = (int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime()); positionView.setMax((int) info.getDuration()); - positionView.setProgress(seconds); + positionView.setProgressAnimated(seconds); detailPositionView.setText(Localization.getDurationString(seconds)); animateView(positionView, true, 500); animateView(detailPositionView, true, 500); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java index 53d549a46..d9c58fbf4 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -101,7 +101,7 @@ public abstract class BaseListFragment extends BaseStateFragment implem updateFlags = 0; } - infoListAdapter.updateStates(); + itemsList.post(infoListAdapter::updateStates); } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java index 3cd06f3d6..7f5b07dbe 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java @@ -312,9 +312,26 @@ public class InfoListAdapter extends StateObjectsListAdapter { } } + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List payloads) { + if (!payloads.isEmpty() && holder instanceof InfoItemHolder) { + for (Object payload : payloads) { + if (payload instanceof StreamStateEntity) { + ((InfoItemHolder) holder).updateState(infoItemList.get(header == null ? position : position - 1), + (StreamStateEntity) payload); + } else if (payload instanceof Boolean) { + ((InfoItemHolder) holder).updateState(infoItemList.get(header == null ? position : position - 1), + null); + } + } + } else { + onBindViewHolder(holder, position); + } + } + @Override protected void onItemStateChanged(int position, @Nullable StreamStateEntity state) { - notifyItemChanged(header == null ? position : position + 1, state); + notifyItemChanged(header == null ? position : position + 1, state != null ? state : false); } public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final int spanCount) { diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/InfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/InfoItemHolder.java index 969d2682e..3bc0d9e54 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/InfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/InfoItemHolder.java @@ -38,4 +38,7 @@ public abstract class InfoItemHolder extends RecyclerView.ViewHolder { } public abstract void updateFromItem(final InfoItem infoItem, @Nullable final StreamStateEntity state); + + public void updateState(final InfoItem infoItem, @Nullable final StreamStateEntity state) { + } } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java index a59cb009f..aa2a3f878 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java @@ -5,7 +5,6 @@ import android.support.v4.content.ContextCompat; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; -import android.widget.ProgressBar; import android.widget.TextView; import org.schabi.newpipe.R; @@ -14,8 +13,10 @@ import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; +import org.schabi.newpipe.views.AnimatedProgressBar; import java.util.concurrent.TimeUnit; @@ -25,7 +26,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder { public final TextView itemVideoTitleView; public final TextView itemUploaderView; public final TextView itemDurationView; - public final ProgressBar itemProgressView; + public final AnimatedProgressBar itemProgressView; StreamMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { super(infoItemBuilder, layoutId, parent); @@ -99,6 +100,22 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder { } } + @Override + public void updateState(final InfoItem infoItem, @Nullable final StreamStateEntity state) { + final StreamInfoItem item = (StreamInfoItem) infoItem; + if (state != null && item.getDuration() > 0 && item.getStreamType() != StreamType.LIVE_STREAM) { + itemProgressView.setMax((int) item.getDuration()); + if (itemProgressView.getVisibility() == View.VISIBLE) { + itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime())); + } else { + itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime())); + AnimationUtils.animateView(itemProgressView, true, 500); + } + } else if (itemProgressView.getVisibility() == View.VISIBLE) { + AnimationUtils.animateView(itemProgressView, false, 500); + } + } + private void enableLongClick(final StreamInfoItem item) { itemView.setLongClickable(true); itemView.setOnLongClickListener(view -> { diff --git a/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java b/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java index 75d49e466..94672bd49 100644 --- a/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java @@ -76,7 +76,8 @@ public abstract class BaseLocalListFragment extends BaseStateFragment } updateFlags = 0; } - itemListAdapter.updateStates(); + + itemsList.post(itemListAdapter::updateStates); } /*////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java index 903712af2..d29e85ee3 100644 --- a/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/local/LocalItemListAdapter.java @@ -285,9 +285,26 @@ public class LocalItemListAdapter extends StateObjectsListAdapter { } } + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List payloads) { + if (!payloads.isEmpty() && holder instanceof LocalItemHolder) { + for (Object payload : payloads) { + if (payload instanceof StreamStateEntity) { + ((LocalItemHolder) holder).updateState(localItems.get(header == null ? position : position - 1), + (StreamStateEntity) payload); + } else if (payload instanceof Boolean) { + ((LocalItemHolder) holder).updateState(localItems.get(header == null ? position : position - 1), + null); + } + } + } else { + onBindViewHolder(holder, position); + } + } + @Override protected void onItemStateChanged(int position, @Nullable StreamStateEntity state) { - notifyItemChanged(header == null ? position : position + 1, state); + notifyItemChanged(header == null ? position : position + 1, state != null ? state : false); } public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final int spanCount) { diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalItemHolder.java index 01af60f98..c00fa1fb4 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalItemHolder.java @@ -41,4 +41,7 @@ public abstract class LocalItemHolder extends RecyclerView.ViewHolder { } public abstract void updateFromItem(final LocalItem item, @Nullable final StreamStateEntity state, final DateFormat dateFormat); + + public void updateState(final LocalItem localItem, @Nullable final StreamStateEntity state) { + } } diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java index 48bbbc81d..0c4e66c9d 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java @@ -6,7 +6,6 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; -import android.widget.ProgressBar; import android.widget.TextView; import org.schabi.newpipe.R; @@ -15,8 +14,10 @@ import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.local.LocalItemBuilder; +import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; +import org.schabi.newpipe.views.AnimatedProgressBar; import java.text.DateFormat; import java.util.concurrent.TimeUnit; @@ -28,7 +29,7 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder { public final TextView itemAdditionalDetailsView; public final TextView itemDurationView; public final View itemHandleView; - public final ProgressBar itemProgressView; + public final AnimatedProgressBar itemProgressView; LocalPlaylistStreamItemHolder(LocalItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) { super(infoItemBuilder, layoutId, parent); @@ -92,6 +93,23 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder { itemHandleView.setOnTouchListener(getOnTouchListener(item)); } + @Override + public void updateState(LocalItem localItem, @Nullable StreamStateEntity state) { + if (!(localItem instanceof PlaylistStreamEntry)) return; + final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem; + if (state != null && item.duration > 0) { + itemProgressView.setMax((int) item.duration); + if (itemProgressView.getVisibility() == View.VISIBLE) { + itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime())); + } else { + itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime())); + AnimationUtils.animateView(itemProgressView, true, 500); + } + } else if (itemProgressView.getVisibility() == View.VISIBLE) { + AnimationUtils.animateView(itemProgressView, false, 500); + } + } + private View.OnTouchListener getOnTouchListener(final PlaylistStreamEntry item) { return (view, motionEvent) -> { view.performClick(); diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java index ac194b776..b24051a4f 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java @@ -5,7 +5,6 @@ import android.support.v4.content.ContextCompat; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; -import android.widget.ProgressBar; import android.widget.TextView; import org.schabi.newpipe.R; @@ -14,8 +13,10 @@ import org.schabi.newpipe.database.stream.StreamStatisticsEntry; import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.local.LocalItemBuilder; +import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.Localization; +import org.schabi.newpipe.views.AnimatedProgressBar; import java.text.DateFormat; import java.util.concurrent.TimeUnit; @@ -48,7 +49,7 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { public final TextView itemDurationView; @Nullable public final TextView itemAdditionalDetails; - public final ProgressBar itemProgressView; + public final AnimatedProgressBar itemProgressView; public LocalStatisticStreamItemHolder(LocalItemBuilder itemBuilder, ViewGroup parent) { this(itemBuilder, R.layout.list_stream_item, parent); @@ -121,4 +122,21 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { return true; }); } + + @Override + public void updateState(LocalItem localItem, @Nullable StreamStateEntity state) { + if (!(localItem instanceof StreamStatisticsEntry)) return; + final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem; + if (state != null && item.duration > 0) { + itemProgressView.setMax((int) item.duration); + if (itemProgressView.getVisibility() == View.VISIBLE) { + itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime())); + } else { + itemProgressView.setProgress((int) TimeUnit.MILLISECONDS.toSeconds(state.getProgressTime())); + AnimationUtils.animateView(itemProgressView, true, 500); + } + } else if (itemProgressView.getVisibility() == View.VISIBLE) { + AnimationUtils.animateView(itemProgressView, false, 500); + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java index 364d50df6..b00ea05ea 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.java @@ -153,6 +153,8 @@ public class SubscriptionFragment extends BaseStateFragment - - - - Date: Sun, 28 Apr 2019 01:29:45 +0200 Subject: [PATCH 087/138] Update version code and name to NewPipe 0.16.2 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index ef50820a2..9e6a21063 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,8 +8,8 @@ android { applicationId "org.schabi.newpipe" minSdkVersion 19 targetSdkVersion 28 - versionCode 730 - versionName "0.16.1" + versionCode 740 + versionName "0.16.2" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true From 46511533aa1c22d04731c8f3d4deabdc9295e0ab Mon Sep 17 00:00:00 2001 From: Jesper Hertel Date: Sat, 27 Apr 2019 10:31:19 +0000 Subject: [PATCH 088/138] Translated using Weblate (Danish) Currently translated at 88.7% (393 of 443 strings) --- app/src/main/res/values-da/strings.xml | 260 ++++++++++++------------- 1 file changed, 130 insertions(+), 130 deletions(-) diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 6143dc6b0..d2cfe3ea4 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -53,7 +53,7 @@ Afspil med Kodi Kore-appen ikke fundet. Installer den\? Vis valgmuligheden \"Afspil med Kodi\" - Vis en knap til at afspille en video via Kodi-mediecenteret + Vis en knap til at afspille en video via Kodi Lyd Standardformat for lydfiler Standardformat for videofiler @@ -61,7 +61,7 @@ Lyst Mørkt Sort - Husk pop op-størrelse og -position + Husk størrelse og placering af pop op Husk sidste størrelse og placering af pop op-afspiller Brug hurtig og upræcis søgning Upræcis søgning lader afspilleren finde placeringer hurtigere, men mindre præcist @@ -73,47 +73,46 @@ Metadata-cache slettet Føj automatisk næste stream til køen Føj automatisk relaterede streams til køen når den sidste stream i en ikke-repeterende kø afspilles. - Juster lydstyrke ved hjælp af fingerkontrol - Brug håndbevægelser til at kontrollere lydstyrke + Juster lydstyrke ved hjælp af fingerbevægelser + Brug fingerbevægelser til at kontrollere lydstyrke Styr lysstyrken med fingerbevægelser Brug fingerbevægelser til at justere afspillerens lysstyrke - Afspiller håndbevægelseskontol - Brug håndbevægelser til at justere lysstyrke og lydstyrke - Søg i foreslag - Vis foreslag når der søges + Fingerbevægelsesstyring af afspiller + Brug fingerbevægelser til at justere lysstyrke og lydstyrke + Søgeforslag + Vis forslag når der søges Søgehistorik Gem søgninger lokalt - Historie & Cache - Hold styr på sete videoer - Forsæt når appen kommer i focus - Foresæt afspilning efter afbrydelse (fks. telefon opkald) + Historik og cache + Husk sete videoer + Fortsæt når appen kommer i fokus + Fortsæt afspilning efter afbrydelser (fx telefonopkald) Download Næste Vis \'Næste\' og \'Lignende\' videoer - Vis \"Hold for at tiljøje\" tip - Vis et tip når baggrunds eller popup knappen trykkes - Dette URL er ikke understøttet - Standard land for indhold - Service - Standard sprog for indhold + Vis \"Hold for at tilføje\"-tip + Vis et tip når der trykkes på baggrunds- eller pop op-knappen på siden med videodetaljer + Denne webadresse er ikke understøttet + Standardland for indhold + Tjeneste + Standardsprog for indhold Afspiller Opførsel - Video & Lyd - Historie & Cache - Popup - "Udseende " + Video og lyd + Historik og cache + Pop op + Udseende Andet - Debug + Fejlretning Opdateringer Afspiller i baggrunden - Afspiller i popup tilstand -\n - Tilføjet til køen i baggrundsafspilleren - Tilføjet til afspilningskøen + Afspiller i pop op-tilstand + Føjet til køen i baggrundsafspilleren + Føjet til pop op-afspilningskøen Afspil - Inhold - Aldersbegrænset inhold - Vis aldersbegrænsede videoer. Du kan tillade den slags videoer under Indstillinger. + Indhold + Aldersbegrænset indhold + Vis aldersbegrænsede videoer. Du kan tillade denne type videoer under Indstillinger. LIVE Downloads Downloads @@ -135,93 +134,90 @@ Filtrer Genindlæs Slet - Ændrer Størrelse + Ændrer størrelse Bedste opløsning Fortryd Fil slettet - Afspil Alle + Afspil alle Altid Kun én gang Fil - NewPipe Notifikation -\n - Notifikationer for NewPipe backgrunds og popup afspillere -\n - App Opdaterings Notifikation - Notifikationer for nye NewPipe versioner -\n + NewPipe-notifikation + Notifikationer for NewPipes baggrunds- og pop op-afspillere + Notifikation om opdatering af app + Notifikationer for nye NewPipe-versioner [Ukendt] - Instill Retning - Skift til Bagrund - Skift til Popup - Skift til Forside + Skift skærmretning + Skift til baggrund + Skift til pop op + Skift til hovedafspiller Importer database Eksporter database - Overskriver din nuværende historie og abonnementer - Eksporter historie, abonnementer og spillelister + Overskriver din nuværende historik og abonnementer + Eksporter historik, abonnementer og spillelister Slet visningshistorik - Sletter historien af viste videoer + Sletter historikken for viste videoer Slet hele visningshistorikken\? Visningshistorikken blev slettet. Slet søgehistorik - Sletter gemte søge ord + Sletter historikken for søgeord Slet hele søgehistorikken\? Søgehistorik slettet. Fejl - Ekstern lagring utilgængelig + Eksternt lager utilgængeligt Download til eksternt SD-kort er endnu ikke muligt. Nulstil placering af download-mappe\? - Netwærksfejl - Kunne indlæse alle billeder - Kunne ikke dekryptere video URL signatur - Kunne ikke indlæse webside - Kunne ikke indlæse hele websiden - Indhold utilgængeligt + Netværksfejl + Kunne ikke indlæse alle miniaturebilleder + Kunne ikke dekryptere URL-signatur for video + Kunne ikke analysere websted + Kunne ikke analysere webstedet fuldstændig + Indhold ikke tilgængeligt Blokeret af GEMA - Kunne ikke oprette download menu - "Livestreams er ikke undestøttet endnu " - Kunne ikke hente nogen streams - Kunne ikke hente billede - App/Brugergrænseflade crashede + Kunne ikke oprette downloadmenu + Livestreams er endnu ikke understøttet + Kunne ikke hente nogen stream + Kunne ikke indlæse billede + App/brugergrænseflade gik ned Kunne ikke afspille denne stream - "Uoprettelig fejl ved opstod ved afspilning " - Prøver at genoprette efter fejl ved afspilning - Eksterne afspillere undestøtter ikke disse typer af links + Uoprettelig afspillerfejl opstod + Prøver at genoprette efter afspillerfejl + Eksterne afspillere understøtter ikke disse typer af links Ugyldig adresse - Ingen video streams blev fundet - Ingen lyd streams blev fundet - Kunne ikke finde mappe - Kunne ikke finde file/kilde - Filen eksister ikke eller også der er manglende rettigheder til at læse og skrive til den + Ingen videostreams fundet + Ingen lydstreams fundet + Mappen findes ikke + Fil eller indholdskilde findes ikke + Filen eksister ikke eller der mangler rettigheder til at læse eller skrive til den Filavnet kan ikke være tomt - Der skete en fejl: %1$s - Ingen streams er tilgængelige til at downloade - Bruger standard tabs, fejl ved indlæsning af gemte tabs + Der opstod en fejl: %1$s + Ingen streams er tilgængelige for download + Bruger standardfaner pga. fejl ved indlæsning af gemte faner Genskab standardindstillinger Vil du genskabe standardindstillingerne\? - Undskyld, dette skulle ikke have været sket. - Rappoter dette via e-mail - Undskyld, der opstod nogle fejl. + Undskyld, dette skulle ikke være sket. + Rapporter fejl via e-mail + Undskyld, nogle fejl opstod. RAPPORTER Information: Hvad skete der: Din kommentar (på engelsk): Detaljer: - Video billede - Video billede + Videominiaturebillede + Videominiaturebillede Uploaders profilbillede Synes godt om Kan ikke lide - Brug TOR - (Eksperimentelt) Tving downloads til at bruge TOR (streaming er ikke understøttet endnu). - Rappoter en Fejl + Brug Tor + (Eksperimentelt) Send downloadtrafik gennem Tor for øget privatliv (videostreaming endnu ikke understøttet). + Rapporter en fejl Ingen resultater Træk for at omarrangere - Kan ikke oprette download mappe \'%1$s\' - Oprettede download mappe \'%1$s\' + Kan ikke oprette downloadmappe \'%1$s\' + Oprettede downloadmappe \'%1$s\' Video Lyd Prøv igen - Adgang til hulkomekse nægtet + Adgang til lager nægtet Ingen abonnenter Antallet af abonnenter er ikke tilgængeligt Ingen visninger @@ -229,86 +225,83 @@ %s visning %s visninger - Ingen Videoer + Ingen videoer Start Afspil Opret Slet Slet én - Slet Alle - Checksum + Slet alle + Kontrolsum Afvis Omdøb Ny mission - Ok + OK Filnavn Tråde Fejl Server ikke understøttet Filen eksisterer allerede - Forkert URL formatering eller manglende internet forbindelse - NewPiper Downloader + Ugyldig webadresse eller manglende internetforbindelse + NewPiper downloader Tryk for detaljer Vent venligst… Kopieret til udklipsholderen - Vælg venligst en ledig download mappe - Denne tilladelse er nødvendig for at kunne åbne i popup tilstand - Et element slettet. - reCAPTCHA -\n - reCAPTCHA udfordring - reCAPTCHA udfordring anmodet + Vælg venligst en tilgængelig downloadmappe + Denne tilladelse er nødvendig for at kunne åbne i pop op-tilstand + 1 element slettet. + reCAPTCHA + reCAPTCHA-udfordring + Der blev anmodet om en reCAPTCHA-udfordring Download - Lovlige tegn i filnavne - Ugyldige tegn bliver erstattet med dette tegn - Erstatningskarakter - Bogstaver og tal + Tilladte tegn i filnavne + Ugyldige tegn bliver erstattet med denne værdi + Erstatningstegn + Bogstaver og cifre De fleste specialtegn Der ingen app installeret der kan afspille denne fil Om NewPipe Indstillinger Om Tredjepartslicenser - © %1$s by %2$s under %3$s -\n + © %1$s af %2$s under %3$s Kunne ikke indlæse licens - Åben webside + Åbn websted Om Bidragsydere Licenser - Libre letvægts streaming på Android. + Åben letvægtsstreaming på Android. Bidrag til projektet - Hvad enten du har idéer til, oversæting, design ændringer, kode refaktorering eller større kode ændringer, så er hjælp altid velkommen. Jo mere der bliver gjort jo bedre bliver det! - Se på Github + Hvad enten du har idéer til oversættelse, designændringer, kodeoprydning eller virkelig tunge kodeændringer, så er hjælp altid velkommen. Jo mere der bliver gjort, jo bedre bliver det! + Se på GitHub Doner - NewPipe er udviklet af frivillige der bruger tid på at give dig den bedste oplevelse. Giv noget tilbage til NewPipes udviklere så de kan gøre appen endnu bedre, mens de nyder en kop kaffe. + NewPipe er udviklet af frivillige der bruger tid på at give dig den bedste oplevelse. Giv noget tilbage for at hjælpe NewPipes udviklere til at gøre appen endnu bedre, mens de nyder en kop kaffe. Giv noget tilbage - Webside - Besøg NewPipes webside for mere information og nyheder. - NewPipes Fortrolighedspolitik -\n + Websted + Besøg NewPipes websted for mere information og nyheder. + NewPipes fortrolighedspolitik Læs fortrolighedspolitik - NewPipe\'s Licens - NewPipe er copyleft libre software: Du kan bruge, studere, dele og forbedre det som du vi.l Specifikt kan du redistribuere og/eller modificere det under betingelserne i GNU General Public License som udgivet af The Free Software Foundation, version 3 af licensen, eller (efter dit valg) en senere version. + NewPipes licens + NewPipe er copyleft, fri software: Du kan bruge, studere, dele og forbedre den som du vil. Specifikt kan du redistribuere og/eller ændre den under betingelserne i GNU General Public License som udgivet af Free Software Foundation, enten version 3 af licensen eller (efter dit ønske) en vilkårlig senere version. Læs licens - Historie + Historik Søgte Sete - Historie er slået fra - Historie - Historien er tom - Historie slettet + Historik er slået fra + Historik + Historikken er tom + Historik slettet Element slettet Vil du slette dette element fra søgehistorikken\? Vil du slette dette element fra visningshistorikken\? Er du sikker på at vil slette alle elementer i historikken\? - Sidst Afspillet - Mest Spillede - Indhold af forsiden - Hvilke tabs vises på forsiden + Sidst afspillet + Mest spillede + Indhold af hovedsiden + Hvilke faner vises på hovedsiden Udvalg - Blank Side - Kiosk Side + Tom side + Kioskside Abonnementsside Kanalside Vælg en kanal @@ -316,24 +309,24 @@ Vælg en kiosk Eksporteret Importeret - Ikke en gyldig ZIP fil + Ikke en gyldig ZIP-fil Advarsel: Kunne ikke importere alle filer. Dette vil overskrive dine nuværende indstillinger. Vil du også importere indstillinger\? Kiosk - Populært Lige Nu + Populært lige nu Top 50 - Nyt & Populært + Nyt og populært Baggrundsafspiller - Popup afspiller + Pop op-afspiller Fjern Detaljer Lydindstillinger - Hold for at tilføje til kø - Tilføj til kø når baggrunds tilstand aktiveres + Hold for at føje til kø + Føj til kø når baggrundstilstand aktiveres Start afspilning her Noget vil dukke op her snart ;D - Foretrukket \'åben\' handling + Foretrukket \'åbn\'-handling Videoafspiller baggrundsafspiller Popup-afspiller @@ -394,7 +387,7 @@ Stop Hændelser Intet at se her - K + T mio. mia. @@ -405,4 +398,11 @@ Feed-side Kunne ikke importere abonnementer Kunne ikke eksportere abonnementer + Brugerrapport + Konferencer + Føj til kø ved ny pop op + Start her når i baggrunden + Start her ved ny pop op + Åbn skuffe + Luk skuffe \ No newline at end of file From 460610f672c0dfed938c74b40e15e8d1f9086bbd Mon Sep 17 00:00:00 2001 From: Chandra Mohan Jha Date: Sat, 27 Apr 2019 07:59:41 +0000 Subject: [PATCH 089/138] Translated using Weblate (Hindi) Currently translated at 77.2% (342 of 443 strings) --- app/src/main/res/values-hi/strings.xml | 29 +++++++++++++------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 9755c26fb..ed2815638 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -57,7 +57,7 @@ क्या आप का मतलब %1$s है? शेयर करें अन्य वीडियो प्लेयर उपयोग करें - कुछ रेसोल्युशनस में नहीं आएगी + कुछ प्रस्तावों पर ऑडियो निकालता है अन्य ऑडियो प्लेयर उपयोग करें मुख्य सदस्यता नहीं बदला जा सका @@ -77,7 +77,7 @@ Kodi से चलाये Kore एप्प नहीं मिला, इसे इनस्टॉल करें? \"Kodi से चलाये\" वाला विकल्प दिखाए - उस विकल्प को दिखाए जिसमे Kodi Media Center के जरिये विडियो प्ले किया जाता है + कोडी मीडिया सेंटर के माध्यम से वीडियो चलाने के लिए एक विकल्प प्रदर्शित करें डिफ़ॉल्ट ऑडियो का फॉर्मेट डिफ़ॉल्ट विडियो का फॉर्मेट WebM - libre फॉर्मेट @@ -111,7 +111,7 @@ चलाये विषयवस्तु उम्र प्रतिबंधित विषय वस्तु - उम्र प्रतिबंदित विडियो है .इस प्रकार की विषयवस्तु को अनुमति देने के लिए Setting से संभव है | + उम्र प्रतिबंदित विडियो है .इस प्रकार की विषयवस्तु को अनुमति देने के लिए सेटिंग से संभव है | सीधा प्रसारण डाउनलोड डाउनलोड @@ -169,7 +169,7 @@ कोई परिणाम नहीं मिला यंहा कुछ नहीं है "डाउनलोड वाली डायरेक्टरी को बना नहीं सकते \'%1$s\'" - डाउनलोड की डायरेक्टरी बना दी गई है \'%1$s\' + डाउनलोड की निर्देशिका बना दी गई है \'%1$s\' विडियो ऑडियो फिर से कोशिश करे @@ -202,7 +202,7 @@ फाइल का नाम मेसेज के thread त्रुटी - server ठीक से चल नहीं रहा + असमर्थित सर्वर फाइल पहले से ही बनी हुई है URL सही नहीं है या फिर हो सकता है इन्टरनेट उपलब्ध नहीं है न्यूपाइप डाउनलोड हो रहा है @@ -212,8 +212,8 @@ कृपया उपलब्ध डाउनलोड फोल्डर को चुने पॉपअप के तरीके में खोलने के लिए अनुमति की जरुरत है reCAPTCHA - reCAPTCHA चुनोती - reCAPTCHA चुनोती के लिए निवेदन + reCAPTCHA चुनौती + reCAPTCHA चुनौती का अनुरोध किया डाउनलोड फाइल के नाम के लिए आवश्यक characters(जैसे - १२३, abc) की अनुमति है अमान्य characters इस value से बदल जायेंगे @@ -238,7 +238,7 @@ kiosk टॉप 50 नया और गरम - बैकग्राउंड प्लेयर + पृष्ठभूमि प्लेयर पॉपअप प्लेयर निकाले विवरण @@ -263,12 +263,12 @@ मुख्य पर स्विच करें डेटाबेस आयात करें डेटाबेस निर्यात करें - आपके वर्तमान इतिहास और सब्सक्रिप्शन को ओवरराइड करेगा + आपके वर्तमान इतिहास और सब्सक्रिप्शन को अधिभावी करेगा इतिहास, सब्सक्रिप्शन और प्लेलिस्ट निर्यात करें एक्सटर्नल प्लेयर इन प्रकार के लिंक सपोर्ट नहीं करता अमान्य URL कोई वीडियो स्ट्रीम नहीं मिला - कोई वीडियो स्ट्रीम नहीं मिला + कोई ऑडियो स्ट्रीम नहीं मिली फिर से क्रम देने के लिए खींचें बनाइये एक को हटाएं @@ -284,8 +284,8 @@ क्या आप वाकई इतिहास से सभी आइटंस हटाना चाहते हैं? पिछला चलाया गया अधिकतम चलाए गए - निर्यात पूर्ण हुआ - आयात पूर्ण हुआ + निर्यात किए गए + आयातित कोई मांय ज़िप फ़ाइल नहीं चेतावनी: सभी फ़ाइलों को आयात नहीं किया जा सका। यह आपके वर्तमान सेटअप को ओवरराइड करेगा । @@ -344,14 +344,14 @@ उपभोगता देखे हुए वीडियो की सूची साफ करें चलाये गए स्ट्रीम का इतिहास साफ करता है - देखे गए सभी का इतिहास साफ करें। + देखे गए सभी का इतिहास साफ करें\? देखे हुए का इतिहास साफ कर दिया गया। ढूंढने के इतिहास को साफ करें ढूंढे गए शब्दो का इतिहास साफ करता है पूरे खोज इतिहास को मिटा दे \? ढूंढने के इतिहास को साफ कर दिया। एसी कोई भी फ़ोल्डर मौजूद नहीं है - अमान्य फाइल/कॉन्टेंट उदभावस्थान + अमान्य फाइल/विषय - वस्तु का स्रोत फ़ाइल मौजूद नहीं है या उसे पढ़ने या लिखने की पर्याप्त अनुमति नही़ं है फ़ाइल का नाम खाली नहीं हो सकता एक भूल हुई: %1$s @@ -407,4 +407,5 @@ नया टॅब टॅब चुने वॉल्यूम नियंत्रण + कतार \ No newline at end of file From c90feaf3dbfdeb68dbe2858b949284bc9f7e156a Mon Sep 17 00:00:00 2001 From: AB Date: Sat, 27 Apr 2019 09:39:53 +0000 Subject: [PATCH 090/138] Translated using Weblate (Ukrainian) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-uk/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 7ab368400..71606b005 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -320,7 +320,7 @@ Створені автоматично Увімкнути LeakCanary Моніторинг втрат пам\'яті може призвести до нереагування застосунку під час запису дампу - Звітувати про помилки життєвого циклу додатку + Звітувати про помилки життєвого циклу застосунку Примусове звітування про неможливість доставлення Rx-винятків, які відбуваються за межами фрагменту або діяльності життєвого циклу після усунення Використовувати швидкий неточний пошук Неточний пошук дозволяє програвачеві рухатися позиціями швидше, проте з меншою точністю @@ -442,10 +442,10 @@ Помилка завантаження Завантаження завершено %s завантажень завершено - Згенерувати унікальне ім\'я + Згенерувати унікальну назву Перезаписати Завантажений файл з таким ім\'ям вже існує - Файл з таким ім\'ям вже завантажується + Файл з такою назвою вже завантажується Показати помилку Код Файл не може бути створений From a684e380b72fb06bc93fc415b0877d99587b161c Mon Sep 17 00:00:00 2001 From: Jesper Hertel Date: Sun, 28 Apr 2019 21:18:11 +0000 Subject: [PATCH 091/138] Translated using Weblate (Danish) Currently translated at 98.9% (438 of 443 strings) --- app/src/main/res/values-da/strings.xml | 86 ++++++++++++++++++++------ 1 file changed, 67 insertions(+), 19 deletions(-) diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index d2cfe3ea4..4dc81f9c2 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -328,39 +328,39 @@ Noget vil dukke op her snart ;D Foretrukket \'åbn\'-handling Videoafspiller - baggrundsafspiller - Popup-afspiller + Baggrundsafspiller + Pop op-afspiller Spørg altid - Henter info… - Indlæser anmodet indhold - Ny Playliste + Henter info … + Indlæser det ønskede indhold + Ny spilleliste Slet Omdøb Navn - Tilføj Til Playliste - Slet denne playliste\? - Playliste oprettet - Ingen Undertekster + Føj til spilleliste + Slet denne spilleliste\? + Spilleliste oprettet + Ingen undertekster Tilpas Udfyld Zoom - Autogeneret + Autogenereret Undertekster Eksporter til Nulstil Accepter Afvis Ingen begrænsning - Begeæns opløsning når der bruges mobil data + Begræns opløsning når der bruges mobildata Opdateringer - Liste tilstand + Listevisning Liste Gitter Automatisk Tryk for at downloade Færdig - i kø - Efterbehandling + I kø + efterbehandling Handling afvist af systemet Download fejlede @@ -368,19 +368,18 @@ %s downloads færdige Generer unikt navn Overskriv - "En downloadet fil med dette navn eksisterer allerede " + En downloadet fil med dette navn eksisterer allerede Der er en download i gang med dette navn Vis fejl Kode Filen kan ikke oprettes - Destinations mappen kan ikke oprettes + Destinationsmappen kan ikke oprettes Adgang nægtet af systemet Sikker forbindelse fejlede - Kan ikke finde serveren + Kunne ikke finde serveren Kan ikke forbinde til serveren Serveren sender ikke data - Serveren accepterer ikke multitrådede downloads, prøv igen med @string/msg_threads = 1 -\n + Serveren accepterer ikke multitrådede downloads; prøv igen med @string/msg_threads = 1 Det anmodede interval er ikke gyldigt Ikke fundet Efterbehandling fejlede @@ -405,4 +404,53 @@ Start her ved ny pop op Åbn skuffe Luk skuffe + Hvad:\\nForespørgsel:\\nIndholdssprog:\\nTjeneste:\\nGMT-tid:\\nPakke:\\nVersion:\\nOS-version: + Standardhandling ved åbning af indhold — %s + Angiv som miniaturebillede for spilleliste + Bogmærk spilleliste + Fjern bogmærke + Føjet til spillelisten + Miniaturebillede for spilleliste ændret. + Kunne ikke slette spilleliste. + Ændr undertekststørrelse og baggrundsstil. Kræver genstart af appen for at træde i kraft. + Aktiver LeakCanary + Monitorering for hukommelseslækager kan få appen til ikke at svare under heap dumping + Rapporter out-of-lifecycle-fejl + Importer/eksporter + Importer + Importer fra + Importerer … + Eksporterer … + Importer fil + Forrige eksport + Importer YouTube-abonnementer ved at downloade eksportfilen: +\n +\n1. Gå til denne webadresse: %1$s +\n2. Log ind når du bliver bedt om det +\n3. En download bør starte (det er eksportfilen) + ditID, soundcloud.com/ditID + Bemærk at denne operation kan kræve meget netværkstrafik. +\n +\nVil du fortsætte\? + Knapper for afspilningshastighed + Tempo + Tonehøjde + Spol forbi stilhed + Skridt + Vis en notifikation for at foreslå opdatering af appen, når en ny version er tilgængelig + Minimer ved appskift + Handling når der skiftes til en anden app fra hovedvideoafspilleren — %s + Ingen + Minimer til baggrundsafspiller + Minimer til pop op-afspiller + Skift visning + NewPipe-opdatering tilgængelig! + sat på pause + sat i kø + Ryd færdige downloads + Fortsæt dine %s ventende overførsler fra Downloads + Maksimalt antal genforsøg + Maksimalt antal forsøg før downloaden opgives + Sæt på pause ved skift til mobildata + Downloads som ikke kan sættes på pause vil blive genstartet \ No newline at end of file From d742ed7b6542720c903e625d5ae0763b8fc79a24 Mon Sep 17 00:00:00 2001 From: yunna Date: Mon, 29 Apr 2019 10:34:14 +0000 Subject: [PATCH 092/138] Translated using Weblate (Japanese) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-ja/strings.xml | 110 ++++++++++++------------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index dd229db93..e1e76475e 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -40,7 +40,7 @@ バックグラウンドで再生中 再生 Torを使用する - (実験的) 強制的に Tor を経由した経路で保存して、匿名性を高めます(動画の同時再生には、まだ対応していません)。 + (実験的) 強制的に Tor を経由した経路で保存して、匿名性を高めます(ストリーミング再生には、まだ対応していません)。 テーマ グレー ホワイト @@ -62,12 +62,12 @@ 生放送にはまだ対応していません コンテンツ 年齢制限のあるコンテンツ - 年齢制限のビデオを表示します。そのような素材の許可は、設定から可能です。 + 年齢制限された動画を表示しています。設定から許可することができます。 ウェブサイトを完全には解析できませんでした 動画を取得できませんでした 申し訳ありません。発生すべきでものではありませんでした。 メールで不具合を報告 - 申し訳ありません、不具合が発生しました + 申し訳ありません、不具合が発生しました。 報告 情報: 発生した内容: @@ -80,7 +80,7 @@ 自動再生 NewPipe が他のアプリから呼び出された時、動画を再生します。 不具合を報告 - 利用者報告 + 利用者レポートを送る 生放送 開始するには検索をタップ 開始 @@ -96,7 +96,7 @@ このサーバーには対応していません ファイルが既に存在します URL の形式が正しくないか、通信が利用できません - NewPipeの保存中 + NewPipeで保存中 タップして詳細を表示 お待ちください… クリップボードにコピーしました @@ -194,7 +194,7 @@ 無効な文字はここで指定した文字に置き換えられます ファイル名の自動修正 文字と数字 - 文字と数字に加え、ほとんどの特殊文字 + 文字と数字と、多くの特殊文字 寄付 NewPipe は、あなたに最高の体験を味わってもらうために、ボランティアが自分たちの時間を使って開発しています。開発者たちがコーヒーを飲みながら NewPipe を継続的に改良できるよう、あなたのご支援をお願いします。 Webサイト @@ -202,22 +202,22 @@ 履歴 検索履歴 再生履歴 - 履歴は無効です + 履歴は無効になっています 履歴 履歴に何もありません - 履歴を消去しました + 履歴を削除しました アイテムを削除しました このアイテムを検索履歴から削除しますか? メインページのコンテンツ 空白ページ - キオスクページ + Kioskページ チャンネル登録ページ フィードページ チャンネルページ - 選択したチャンネル + チャンネルを選択 購読しているチャンネルはありません - 選択したキオスク - キオスク + Kioskを選択 + Kiosk 人気 トップ50 ポップアップ再生 @@ -240,8 +240,8 @@ 再生エラーからの回復中 外部プレーヤーは、これらのタイプのリンクをサポートしていません 無効なURL - エクスポートが完了 - インポートが完了 + エクスポートしました + インポートしました 有効な ZIP ファイルではありません 警告: すべてのファイルをインポートできませんでした。 これにより、現在の設定が上書きされます。 @@ -295,7 +295,7 @@ プレイリストが作成されました プレイリストに追加しました プレイリストのサムネイルを変更しました。 - プレイリストが削除できませんでした。 + プレイリストを削除できませんでした。 字幕なし 字幕の文字サイズ インポート/エクスポート @@ -308,7 +308,7 @@ 前回のエクスポート 購読リストがインポートできませんでした 購読リストがエクスポートできませんでした - テンポ + 速度 音程 デフォルト バックグラウンド再生の順番待ちに追加 @@ -320,7 +320,7 @@ 検索キーワードの履歴を削除します 検索履歴を削除しました。 このファイルを再生するためのアプリがインストールされていません - 設定をインポートしますか? + 設定もインポートしますか? 字幕 チャンネル プレイリスト @@ -355,38 +355,38 @@ 「長押しして追加」のヒントを表示 トラック NewPipe バックグラウンドおよびポップアップのプレーヤーの通知 - 新しい & ホット + 新着 & 人気 長押ししてキューに入れる バックグラウンド時にキューに入れる ポップアップ時にキューに入れる - 新ポップアップ時にここから開始 + ポップアップ時にここから開始 すぐにここに表示されます;D お好みの \'開く\' アクション コンテンツを開くときのデフォルト動作 — %s フィット - 埋める + 全画面 自動生成 - プレーヤーのキャプション文字スケールと背景スタイルを変更します。有効にするにはアプリの再起動が必要です。 + 字幕の文字サイズと背景色を変更します。有効にするにはアプリの再起動が必要です。 何もありません - 保存したエクスポートファイルからYoutubeの購読をインポート: -\n -\n1. このURLを開きます: %1$s -\n2. ログインしていなければログインします + 保存したエクスポートファイルからYouTubeの購読をインポート: +\n +\n1. このURLを開きます: %1$s +\n2. ログインしていなければログインします \n3. ダウンロードが始まります (これがエクスポートファイルです) リセット 同意する - 拒否 + 拒否する 制限なし モバイルデータ使用時の解像度の制限 アプリ切り替え時の最小化 - プレーヤーから他アプリに切り替え時の動作 — %s + プレーヤーから他のアプリに切り替え時の動作 — %s 何もしない バックグラウンドに変更 ポップアップに変更 LeakCanary を有効にする - メモリリークの監視は、ヒープのダンピング時にアプリが無反応になる原因となります + メモリリークの監視は、ヒープダンピング時にアプリが無反応になる原因となります ライフサイクルエラーの報告 - 破棄後のフラグメントまたはアクティビティライフサイクル外の到達不能RX例外を強制的に報告します + 破棄されたフラグメントまたはアクティビティの、ライフサイクル範囲外での配信不能なRx例外を強制的に報告します URL または ID を入力して SoundCloud プロファイルをインポートします: \n \n1. Web ブラウザーで \"デスクトップモード\" を有効にします (サイトは携帯デバイスで利用できません) @@ -394,13 +394,13 @@ \n3. 必要に応じてログインします \n4. リダイレクトされたプロファイル URL をコピーします。 あなたのID, soundcloud.com/あなたのid - この操作により通信料金が増えることがあることにご注意ください。 -\n + この操作により通信料金が増えることがあります。ご注意ください。 +\n \n続行しますか\? - 再生速度制御 - リンク解除 (不備が生じる可能性があります) + 再生速度を変更 + 連動しない (歪むかもしれません) 無音の間に早送り - ステップ + 音程幅 購読解除 新しいタブ タブを選択 @@ -414,46 +414,46 @@ 選択 会議 ヨーロッパの一般データ保護規制(GDPR)に準拠するために、NewPipeの個人情報保護方針にご注意ください。よく読んでください。 -\nあなたは私たちにバグレポートを送るためにそれを受け入れなければなりません。 +\n私たちに不具合報告を送るためには、これを受け入れなければなりません。 更新 - リスト ビュー モード + リストビュー モード リスト グリッド 自動 スイッチビュー - 利用可能なNewPipe更新! - ダウンロードするをタップ + NewPipeのアップデートがあります! + タップでダウンロード 終了しました - 順番待ち + 順番に処理中 一時停止 - 待ち行列 - 後処理 - キュー - システムによって拒否されたアクション + 順番待ちに追加しました + 保存処理をしています + 順に処理する + 操作がシステムによって拒否されました ダウンロードに失敗しました ダウンロードが完了しました %s 件のダウンロード終了 一意の名前を生成します 上書き - この名前のファイルが既に存在します - この名前を持つ進行中のダウンロードがあります - エラーを表示します + 同じ名前のファイルが既に存在します + 同じ名前を持つダウンロードが既に進行中です + エラーを表示する コード ファイルを作成できません 宛先フォルダを作成できません - システムが拒否する許可 + 権限がシステムによって拒否されました 安全な接続に失敗しました - サーバが見つからなかった + サーバが見つかりませんでした サーバに接続できません - サーバーはデータを送信しません - サーバーは、マルチ スレッドのダウンロードを受け付けない、再試行してください @string/msg_threads = 1 + サーバがデータを送信していません + サーバが同時接続ダウンロードを受け付けません。再試行してください @string/msg_threads = 1 必要な範囲が満たされていません 見つかりません - 後処理失敗 - 明確な完成したダウンロード + 保存処理に失敗しました + 完了済みのダウンロードを削除 停止 最大再試行回数 - ダウンロードをキャンセルする前に試行の最大数 + ダウンロードを中止するまでの最大再試行回数 音量ジェスチャー制御 ジェスチャーを使用して、プレーヤーの音量を制御します 明るさのジェスチャー制御 @@ -465,6 +465,6 @@ メインページに表示されるタブ 新しいバージョンが利用可能なときにアプリの更新を確認する通知を表示します ダウンロードから %s の保留中の転送を続行します - モバイルデータ通信に切り替え時に休止 - 休止できないダウンロードが再開されます + モバイルデータ通信に切替時に、一時停止する + 一時停止できないダウンロードは再開されます \ No newline at end of file From 0ab29b7c1f15cbf99ef4edff96c2ba267c189e4a Mon Sep 17 00:00:00 2001 From: Snow K1ng Date: Sun, 28 Apr 2019 20:19:57 +0000 Subject: [PATCH 093/138] Translated using Weblate (Romanian) Currently translated at 75.8% (336 of 443 strings) --- app/src/main/res/values-ro/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 26dcce0c3..5c0f0e41d 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -128,7 +128,7 @@ pentru a deschide în mod pop-up K mil. mld. - Anumite rezoluții NU vor avea sunet atunci când această opțiune este activată + Elimina sunetul la anumite rezolutii. Fundal Pop-up Reține dimensiunea și poziția pop-up-ului From ff52fe4884233049e513d099a4a17050ba1d2cea Mon Sep 17 00:00:00 2001 From: Tobias Groza Date: Fri, 3 May 2019 07:58:39 +0200 Subject: [PATCH 094/138] Add changelog for NewPipe 0.16.2 --- .../metadata/android/en-US/changelogs/740.txt | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 fastlane/metadata/android/en-US/changelogs/740.txt diff --git a/fastlane/metadata/android/en-US/changelogs/740.txt b/fastlane/metadata/android/en-US/changelogs/740.txt new file mode 100644 index 000000000..b0f0538ba --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/740.txt @@ -0,0 +1,21 @@ +

Improvements

+
    +
  • make links in comments clickable, increase text size
  • +
  • seek on clicking timestamp links in comments
  • +
  • show preferred tab based on recently selected state
  • +
  • add playlist to queue when long clicking on 'Background' in playlist window
  • +
  • search for shared text when it is not an URL
  • +
  • add "share at current time" button to the main video player
  • +
  • add close button to main player when video queue is finished
  • +
  • add "Play directly in Background" to longpress menu for video list items
  • +
  • improve English translations for Play/Enqueue commands
  • +
  • small performance improvements
  • +
  • remove unused files
  • +
  • update ExoPlayer to 2.9.6
  • +
  • add support for Invidious links
  • +
+

Fixed

+
    +
  • fixed scroll w/ comments and related streams disabled
  • +
  • fixed CheckForNewAppVersionTask being executed when it shouldn't
  • +
From e4f9af70760f819fdd50825fcc390105c0e44eef Mon Sep 17 00:00:00 2001 From: Jonathan Tavares Date: Sun, 5 May 2019 00:57:17 +0000 Subject: [PATCH 095/138] Translated using Weblate (Portuguese (Brazil)) Currently translated at 98.6% (437 of 443 strings) --- app/src/main/res/values-pt-rBR/strings.xml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 6ec9ced28..6a874a98e 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -7,7 +7,7 @@ Informações: %1$s visualizações Reproduzir - Mostar vídeo com restrição de idade. Permissão para vídeos com essa restrição podem ser feitas no menu Configurações. + Mostrar vídeo com restrição de idade. Permissão para vídeos com essa restrição podem ser feitas no menu Configurações. Vídeo Reproduz o vídeo quando o NewPipe for aberto a partir de outro aplicativo Reproduzir automaticamente @@ -133,7 +133,7 @@ abrir em modo popup Atualizar Limpar Popup - Plano de fundo + Segundo plano Lembrar tamanho e posição do popup Lembrar do último tamanho e posição definido para o popup Popup @@ -227,8 +227,8 @@ abrir em modo popup Top 50 Novos e tendências Mostrar dica \"Mantenha pressionado para enfileirar\" - Mostrar dica quando o botão de plano de fundo ou de popup for pressionado na página de detalhes do vídeo - Adicionado a fila do reprodutor em plano de fundo + Mostrar dica quando o botão de segundo plano ou de popup for pressionado na página de detalhes do vídeo + Adicionado a fila do reprodutor em segundo plano Adicionado a fila no reprodutor popup Reproduzir tudo Não foi possível reproduzir esta stream @@ -257,7 +257,7 @@ abrir em modo popup Sempre Apenas esta vez Alterar a orientação - Alterar para Plano de Fundo + Alternar para Segundo Plano Alterar para Popup Alterar para o Principal Reprodutores externos não suportam estes tipos de links @@ -458,13 +458,13 @@ abrir em modo popup Não foi possível conectar ao servidor O servidor não envia dados "O servidor não aceita downloads em multi-thread, tente com @string/msg_threads = 1 " - Intervalo solicitado não satisfazível + Intervalo solicitado não aceito. Não encontrado Falha no pós processamento Limpar downloads finalizados Continuar seus %s downloads pendentes Parar - Máximo de tentativas + Tentativas máximas Número máximo de tentativas antes de cancelar o download Pausar quando trocar para dados móveis Downloads que não puderem ser pausados serão reiniciados From 55d6825f63beb374b620a6fcdab15b024d22a0ed Mon Sep 17 00:00:00 2001 From: abvgeej Date: Sun, 5 May 2019 11:13:10 +0000 Subject: [PATCH 096/138] Translated using Weblate (Russian) Currently translated at 99.5% (441 of 443 strings) --- app/src/main/res/values-ru/strings.xml | 76 +++++++++++++------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index ce488455c..27d2d9cd6 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -7,16 +7,16 @@ Отмена Открыть в браузере Поделиться - Загрузить + Скачать Поиск Настройки Возможно, вы имели в виду: %1$s? Поделиться в Выбор браузера поворот - Путь загрузки видео - Путь для сохранения загруженных видео - Введите путь к папке для загрузки видео + Путь для скачанного видео + Путь для сохранения скачанного видео + Введите путь к папке для скачивания видео Разрешение по умолчанию Воспроизвести в Kodi Приложение Kore не найдено. Установить его? @@ -24,10 +24,10 @@ Показать опцию воспроизведения через медиацентр Kodi Аудио Формат аудио по умолчанию - Загрузить - Следующее видео + Скачать + Следующее URL не поддерживается - \"Следующее\" и \"Похожие\" видео + Показывать \"Следующее\" и \"Похожие\" видео Язык контента по умолчанию Видео и аудио Внешний вид @@ -39,16 +39,16 @@ Понравилось Внешний видеоплеер Внешний аудиоплеер - Воспроизведение в фоновом режиме + Фоновое воспроизведение Тема Тёмная Светлая Воспроизвести Ошибка сети Использовать Tor - Путь загрузки аудио - Папка для хранения загруженных аудио - Введите путь к папке для загрузки аудио + Путь для скачанного аудио + Папка для хранения скачанного аудио + Введите путь к папке для скачанного аудио Начните с поиска Подождите… Файл уже существует @@ -66,9 +66,9 @@ Подробнее Скопировано в буфер обмена Выберите доступную папку для загрузки - Контент 18+ + Ограниченный контент Ошибка - Ваш комментарий (English): + Ваш комментарий (по Английски): Не удалось создать папку для загрузки \"%1$s\" Автовоспроизведение Воспроизводить видео при вызове NewPipe из другого приложения @@ -83,9 +83,9 @@ Контент недоступен Заблокировано GEMA Не удалось создать меню загрузки - Это прямая трансляция, пока не поддерживается + Прямые трансляции пока не поддерживается Не удалось загрузить изображение - Падение приложения/UI + Приложение упало Простите, это не должно было произойти. Отправить отчёт по e-mail Простите, произошли ошибки. @@ -105,8 +105,8 @@ Не удалось расшифровать подпись URL у видео Не удалось найти ни одного потока Воспроизвести - В фоне - В окне + Фоновое воспроизведение + Всплывающее окно Только некоторые устройства поддерживают видео в 2K/4K Формат видео по умолчанию Чёрная @@ -126,23 +126,23 @@ Это разрешение нужно для \nвоспроизведения в окне reCAPTCHA - Открыть в режиме окна - Отображать подсказки при поиске + Открыть во всплывающем окне + Отображать предложения при поиске Позже Отключено Изменение размера - В некоторых разрешениях НЕ будет звука, если включено + Убирает звук в НЕКОТОРЫХ разрешениях  млн.  млрд.  тыс. - Разрешение очереди \"В окне\" - Помнить последние размер и позицию \"В окне\" - Варианты поиска + Разрешение очереди всплывающего окна + Помнить последние размер и позицию всплывающего окна + Поисковые предложения Лучшее разрешение Запрос reCAPTCHA Запрошен ввод reCAPTCHA Высокие разрешения - NewPipe в окне + NewPipe во всплывающем окне О NewPipe Настройки Лицензия NewPipe @@ -225,8 +225,8 @@ Тренды Топ 50 Новое и горячее - Добавлено в очередь в фоне - Добавлено в очередь в окне + Добавлено в очередь на фоновое воспроизведение + Добавлено в очередь во всплывающем окне Играть всё Не удалось воспроизвести этот поток Сведения @@ -235,8 +235,8 @@ Удалить Вы подписаны Подписка отменена - \"Зажмите, чтобы добавить\" - Показать подсказку при нажатии \"В окне\" или \"В фоне\" на странице сведений о видео + Показывать \"Зажмите, чтобы добавить\" + Показывать подсказку при нажатии кнопок всплывающего окна или фонового воспроизведения на странице сведений о видео [Неизвестно] Восстановление после ошибки плеера В фоне @@ -259,8 +259,8 @@ Ошибка плеера без возможности восстановления Внешние плееры не поддерживают ссылки этих типов Неверная ссылка - Потоки видео не найдены - Потоки аудио не найдены + Видеопотоки не найдены + Аудиопотоки не найдены Пожертвовать Разработчики NewPipe ценой своего свободного времени делают вашу жизнь чуть удобнее. Отплатите им тем же — наслаждаясь чашечкой кофе, они смогут сделать NewPipe ещё лучше. Воздать должное @@ -273,22 +273,22 @@ Плеер в окне Получение сведений… Загрузка запрошенного контента - Загрузить файл прямой трансляции + Скачать файл прямой трансляции Показать сведения Закладки Добавить к Быстрый поиск позиции - Неточный поиск позволяет плееру искать позицию быстрее, но менее точно + Позволяет искать позицию быстрее, но с меньшей точностью Автодополнение очереди - Добавлять похожие потоки в очередь при воспроизведении последнего потока, если не включён повтор + Добавляет похожие потоки в очередь при воспроизведении последнего потока, если не включён повтор Отладка Файл Импорт данных Экспорт данных Текущие подписки, плейлисты и история будут заменены Экспорт подписок, плейлистов и истории - Неверная папка - Неверный файл или источник контента + Папка не существует + Папка или источник контента не существуют Файл не существует или нет разрешения на его чтение или запись Имя файла не может быть пустым Произошла ошибка: %1$s @@ -368,7 +368,7 @@ Независимо (искажения) Удалить все загруженные данные веб-страниц При открытии ссылки на контент — %s - Нет потоков, доступных для загрузки + Нет потоков для загрузки Титры Приложение для воспроизведения этого файла не установлено Изменить размер текста и стиль титров. Нужен перезапуск @@ -416,8 +416,8 @@ Загрузка на внешний накопитель пока невозможна. Сбросить расположение папки загрузки\? Внешний накопитель недоступен Вкладки, видимые на главной странице - По умолчанию - Хотите восстановить умолчания? + Восстановить значения по умолчанию + Хотите восстановить значения по умолчанию\? Ошибка чтения сохранённых вкладок. Используются вкладки по умолчанию Выбор Количество подписчиков недоступно From 312e1378d3510ba9be4a23eba4a5f148163a8ccb Mon Sep 17 00:00:00 2001 From: Vasiliy Date: Mon, 6 May 2019 19:16:39 +0300 Subject: [PATCH 097/138] Fix tablet ui --- .../res/layout-large-land/fragment_video_detail.xml | 11 +++++------ .../res/layout/list_stream_playlist_grid_item.xml | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/src/main/res/layout-large-land/fragment_video_detail.xml b/app/src/main/res/layout-large-land/fragment_video_detail.xml index f773c69cf..15d6b7a17 100644 --- a/app/src/main/res/layout-large-land/fragment_video_detail.xml +++ b/app/src/main/res/layout-large-land/fragment_video_detail.xml @@ -6,7 +6,9 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:focusableInTouchMode="true" - android:orientation="horizontal"> + tools:ignore="RtlHardcoded" + android:orientation="horizontal" + android:baselineAligned="false"> @@ -177,7 +178,6 @@ android:paddingBottom="8dp" android:textAppearance="?android:attr/textAppearanceLarge" android:textSize="@dimen/video_item_detail_title_text_size" - tools:ignore="RtlHardcoded" tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed a ultricies ex. Integer sit amet sodales risus. Duis non mi et urna pretium bibendum. Nunc eleifend est quis ipsum porttitor egestas. Sed facilisis, nisl quis eleifend pellentesque, orci metus egestas dolor, at accumsan eros metus quis libero." /> + tools:ignore="ContentDescription" /> @@ -253,8 +253,7 @@ android:layout_width="@dimen/video_item_detail_uploader_image_size" android:layout_height="@dimen/video_item_detail_uploader_image_size" android:contentDescription="@string/detail_uploader_thumbnail_view_description" - android:src="@drawable/buddy" - tools:ignore="RtlHardcoded" /> + android:src="@drawable/buddy" /> - Date: Tue, 7 May 2019 13:56:42 +0000 Subject: [PATCH 098/138] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-pt-rBR/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 6a874a98e..616916298 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -7,7 +7,7 @@ Informações: %1$s visualizações Reproduzir - Mostrar vídeo com restrição de idade. Permissão para vídeos com essa restrição podem ser feitas no menu Configurações. + Exibir vídeo com restrições de idade. Permissão para vídeos com essa restrição pode ser dada no menu Configurações. Vídeo Reproduz o vídeo quando o NewPipe for aberto a partir de outro aplicativo Reproduzir automaticamente @@ -227,8 +227,8 @@ abrir em modo popup Top 50 Novos e tendências Mostrar dica \"Mantenha pressionado para enfileirar\" - Mostrar dica quando o botão de segundo plano ou de popup for pressionado na página de detalhes do vídeo - Adicionado a fila do reprodutor em segundo plano + Exibir dica quando o botão de segundo plano ou de popup for pressionado na página de detalhes do vídeo + Adicionado à fila do reprodutor em segundo plano Adicionado a fila no reprodutor popup Reproduzir tudo Não foi possível reproduzir esta stream From b05e3ca8d8c04ded3f4f3b10bc2582f5738c7b72 Mon Sep 17 00:00:00 2001 From: Eduardo Caron Date: Fri, 10 May 2019 01:01:38 +0000 Subject: [PATCH 099/138] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-pt-rBR/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 616916298..798131f37 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -458,7 +458,7 @@ abrir em modo popup Não foi possível conectar ao servidor O servidor não envia dados "O servidor não aceita downloads em multi-thread, tente com @string/msg_threads = 1 " - Intervalo solicitado não aceito. + Intervalo solicitado não aceito Não encontrado Falha no pós processamento Limpar downloads finalizados From d63c18f7f08df6b26a4e3c4421f69afd6e118bdd Mon Sep 17 00:00:00 2001 From: 84436 Date: Thu, 9 May 2019 15:28:51 +0000 Subject: [PATCH 100/138] Translated using Weblate (Vietnamese) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-vi/strings.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index f200bdd72..af23f383c 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -19,7 +19,7 @@ Âm thanh có thể không có ở *một vài* độ phân giải video Sử dụng trình phát audio bên ngoài Chế độ popup của NewPipe - + Trình phát nổi Thư mục tải về video Đường dẫn để lưu video đã tải về Nhập đường dẫn tải về cho video @@ -160,7 +160,7 @@ xoay màn hình Ngôn ngữ nội dung ưu tiên Video & âm thanh - + Trình phát nổi Lịch sử & bộ nhớ cache Lịch sử & bộ nhớ cache Playlist @@ -419,7 +419,7 @@ Số người đăng ký không khả dụng Chọn các tab để hiện trên trang chủ Lựa chọn - + Top 50 phổ biến Nhập ID SoundCloud hoặc link soundcloud.com/<ID của bạn> Cập nhật Hiện thông báo khi có bản cập nhật ứng dụng @@ -453,7 +453,7 @@ Không thế kết nối với máy chủ Máy chủ không gửi dữ liệu về Máy chủ không chấp nhận tải đa luồng, thử lại với số luồng = 1 - Không thể đáp ứng dãy đã yêu cầu + (HTTP) Không thể đáp ứng khoảng dữ liệu đã yêu cầu Không tìm thấy Xử lý thất bại Dọn các tải về đã hoàn thành From 72eae64698aeb6ce2b76ba2c4106b587f678235a Mon Sep 17 00:00:00 2001 From: Tolstovka Date: Sun, 12 May 2019 19:13:05 +0000 Subject: [PATCH 101/138] Translated using Weblate (Russian) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-ru/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 27d2d9cd6..cc88743ff 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -32,8 +32,8 @@ Видео и аудио Внешний вид Другое - Миниатюра видео-превью - Миниатюра видео-превью + Изображение видео перед его просмотром + Изображение видео перед его просмотром Миниатюра аватара пользователя Не понравилось Понравилось From a758267d724f6f446bbdbd84cddb9fee67a85432 Mon Sep 17 00:00:00 2001 From: Tolstovka Date: Sun, 12 May 2019 19:13:40 +0000 Subject: [PATCH 102/138] Translated using Weblate (Ukrainian) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-uk/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 71606b005..ffc996f49 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -80,8 +80,8 @@ Застосунок/інтерфейс впав Ваш коментар (англійською): Подробиці: - Ескіз попереднього перегляду відео - Ескіз попереднього перегляду відео + Зображення відео перед його переглядом + Зображення відео перед його переглядом Використовувати Tor (Експериментально) Перенаправляти трафік через Tor для підвищення конфіденційності (трансляція відео ще не підтримується). Повідомити про помилку @@ -219,7 +219,7 @@ Сталася невиправна помилка програвача Зовнішні програвачі не підтримують такі види посилань Що:\\nЗапит:\\nМова контенту:\\nСервіс:\\nЧас GMT:\\nПакунок:\\nВерсія:\\nВерсія ОС: - Ескіз аватару користувача + Мініатюрний аватар завантажувача відео Сподобалося Не сподобалося Нічого не знайдено From 66b26e52ce4554c4425d27b0a057d6c69a62b34f Mon Sep 17 00:00:00 2001 From: Tobias Groza Date: Tue, 14 May 2019 23:16:43 +0200 Subject: [PATCH 103/138] Update extractor version --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 9e6a21063..da735302c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -57,7 +57,7 @@ dependencies { exclude module: 'support-annotations' }) - implementation 'com.github.TeamNewPipe:NewPipeExtractor:aa4f03a' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:2ac713e' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:2.23.0' From 47779106441c693fe184ae4c8841adb75c16c60b Mon Sep 17 00:00:00 2001 From: Tobias Groza Date: Tue, 14 May 2019 23:17:04 +0200 Subject: [PATCH 104/138] Update changelog for 0.16.2 --- fastlane/metadata/android/en-US/changelogs/740.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fastlane/metadata/android/en-US/changelogs/740.txt b/fastlane/metadata/android/en-US/changelogs/740.txt index b0f0538ba..c795978a8 100644 --- a/fastlane/metadata/android/en-US/changelogs/740.txt +++ b/fastlane/metadata/android/en-US/changelogs/740.txt @@ -18,4 +18,6 @@
  • fixed scroll w/ comments and related streams disabled
  • fixed CheckForNewAppVersionTask being executed when it shouldn't
  • +
  • fixed youtube subscription import: ignore ones with invalid url and keep ones with empty title
  • +
  • fix invalid YouTube url: signature tag name is not always "signature" preventing streams from loading
From bd2c65cd94f5204d1fea30bdb3d887a09095242d Mon Sep 17 00:00:00 2001 From: Marc Riera Date: Tue, 14 May 2019 08:40:15 +0000 Subject: [PATCH 105/138] Translated using Weblate (Catalan) Currently translated at 96.8% (429 of 443 strings) --- app/src/main/res/values-ca/strings.xml | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 9953766e1..5ac2f2134 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -37,7 +37,7 @@ Depuració Contingut Desactiva les restriccions per edat - Vídeo restringit per edat. Podeu permetre aquesta mena de continguts des de la configuració. + Mostra el vídeo restringit per edat. Podeu permetre aquesta mena de continguts des de la configuració. EN DIRECTE Baixades Baixades @@ -437,4 +437,26 @@ No es pot crear el fitxer No es pot crear la carpeta de destinació Atura + Esdeveniments + Notificacions de noves versions del NewPipe + El nombre de subscriptors no està disponible + Quines pestanyes es mostren a la pàgina principal + Conferències + Mode de vista de llista + Finalitzades + post-processament + Ha fallat la baixada + Baixada finalitzada + %s baixades finalitzades + Ja existeix un fitxer baixat amb aquest nom + Hi ha una baixada en curs amb aquest nom + Ha fallat la connexió segura + No s\'ha pogut trobar el servidor + No s\'ha pogut connectar amb el servidor + Ha fallat el post-processament + Neteja les baixades finalitzades + Intents màxims + Nombre màxim d\'intents abans de cancel·lar la baixada + Pausa en canviar a dades mòbils + Les baixades que no es puguin pausar es tornaran a iniciar \ No newline at end of file From f6068dc69d5aae4d94acaee0c8effb984b52c6f4 Mon Sep 17 00:00:00 2001 From: Tobias Groza Date: Fri, 24 May 2019 21:45:15 +0000 Subject: [PATCH 106/138] Translated using Weblate (German) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-de/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index f83ed7b65..4578040d5 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -349,7 +349,7 @@ Verknüpfung aufheben (kann zu Verzerrungen führen) Nightcore Standard - Abschalten, um das Laden von Miniaturansichten zu verhindern, was Daten- und Speicherverbrauch spart. Änderungen löschen den Bildzwischenspeicher sowohl im Arbeitsspeicher als auch auf der Festplatte. + Abschalten, um das Laden von Miniaturansichten zu verhindern, was Daten- und Speicherverbrauch spart. Änderungen löschen den Bildzwischenspeicher sowohl im Arbeitsspeicher als auch auf dem internen Speicher. Nächsten Stream automatisch einreihen Automatisches Anhängen eines verwandten Streams beim Abspielen des letzten Streams in einer nicht wiederholten Warteschlange. Hier wird bald etwas stehen ;D From f26915aab68577460ef77db0d8d7620335ebad18 Mon Sep 17 00:00:00 2001 From: Davide Palma Date: Thu, 23 May 2019 11:47:36 +0000 Subject: [PATCH 107/138] Translated using Weblate (Italian) Currently translated at 99.8% (442 of 443 strings) --- app/src/main/res/values-it/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index ad9021f75..b4bcd2926 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -154,7 +154,7 @@ Informazioni Contributori Licenze - Interfaccia libera e leggera per lo streaming di YouTube su Android. + Streaming libero e leggero su Android. Mostra su GitHub Licenza di NewPipe Un aiuto è sempre il benvenuto: nuove idee e funzionalità, traduzioni, modifiche al design o cambiamenti radicali del codice rendono l\'applicazione sempre migliore! @@ -260,7 +260,7 @@ Servizio Apri il menu Chiudi il menu - Nessun lettore multimediale trovato. Puoi installare VLC per riprodurlo. + Nessun lettore multimediale trovato (puoi installare VLC per riprodurlo). Sempre Solo una volta I lettori multimediali esterni non supportano questa tipologia di collegamenti @@ -362,7 +362,7 @@ \n \nVuoi continuare?
Carica miniature - Disabilita il caricamento delle miniature, risparmiando dati e memoria. La modifica dell\'opzione comporta la cancellazione della cache in memoria e sul disco. + Disabilita per prevenire il caricamento delle miniature, risparmiando dati e memoria. La modifica di questa opzione cancellerà la cache delle immagini in memoria e sul disco. Pulizia della cache delle immagini completata Pulisci la cache dei metadati Rimuovi tutti i dati delle pagine web memorizzati nella cache From 0c354c4fdb2a4cfe4c55f5b7d28814145b488e4a Mon Sep 17 00:00:00 2001 From: Tobias Groza Date: Mon, 27 May 2019 00:11:37 +0200 Subject: [PATCH 108/138] Remove old strings Remove strings which have been deleted from the English strings file during development, but were translated via Weblate, which failed to pull and push our upstream repo. --- app/src/main/res/values-b+ast/strings.xml | 8 ++------ app/src/main/res/values-be/strings.xml | 4 ---- app/src/main/res/values-bg/strings.xml | 2 -- app/src/main/res/values-bn-rBD/strings.xml | 2 -- app/src/main/res/values-ca/strings.xml | 2 -- app/src/main/res/values-cmn/strings.xml | 4 ---- app/src/main/res/values-cs/strings.xml | 11 ++--------- app/src/main/res/values-de/strings.xml | 3 --- app/src/main/res/values-el/strings.xml | 4 ---- app/src/main/res/values-es/strings.xml | 1 - app/src/main/res/values-et/strings.xml | 2 -- app/src/main/res/values-eu/strings.xml | 3 --- app/src/main/res/values-fa/strings.xml | 3 --- app/src/main/res/values-fi/strings.xml | 2 -- app/src/main/res/values-fr/strings.xml | 3 --- app/src/main/res/values-gl/strings.xml | 4 ---- app/src/main/res/values-he/strings.xml | 2 -- app/src/main/res/values-hi/strings.xml | 7 ------- app/src/main/res/values-hr/strings.xml | 2 -- app/src/main/res/values-hu/strings.xml | 5 ----- app/src/main/res/values-id/strings.xml | 4 ---- app/src/main/res/values-it/strings.xml | 5 +---- app/src/main/res/values-ja/strings.xml | 3 --- app/src/main/res/values-ko/strings.xml | 3 --- app/src/main/res/values-lt/strings.xml | 2 -- app/src/main/res/values-mk/strings.xml | 2 -- app/src/main/res/values-nb-rNO/strings.xml | 8 ++------ app/src/main/res/values-nl-rBE/strings.xml | 2 -- app/src/main/res/values-nl/strings.xml | 3 --- app/src/main/res/values-sk/strings.xml | 2 -- app/src/main/res/values-sl/strings.xml | 2 -- app/src/main/res/values-sr/strings.xml | 2 -- app/src/main/res/values-ta/strings.xml | 1 - app/src/main/res/values-te/strings.xml | 1 - app/src/main/res/values-tr/strings.xml | 3 --- app/src/main/res/values-uk/strings.xml | 3 +-- app/src/main/res/values-vi/strings.xml | 2 -- app/src/main/res/values-zh-rCN/strings.xml | 2 -- app/src/main/res/values-zh-rHK/strings.xml | 2 -- app/src/main/res/values-zh-rTW/strings.xml | 6 ------ 40 files changed, 8 insertions(+), 124 deletions(-) diff --git a/app/src/main/res/values-b+ast/strings.xml b/app/src/main/res/values-b+ast/strings.xml index f9834119d..5f482809a 100644 --- a/app/src/main/res/values-b+ast/strings.xml +++ b/app/src/main/res/values-b+ast/strings.xml @@ -134,8 +134,6 @@ Más sero Desactivóse - Usar reproductor vieyu - M Mill @@ -168,8 +166,7 @@ Ventanu emerxente Redimensionáu - Reproductor vieyu integráu de Mediaframework -Soscribise + Soscribise Soscribiéstite Desoscribiéstite de la canal Nun pue camudase la resolución @@ -325,8 +322,7 @@ Reproductor de videu Reproductor en segundu planu Reproductor en ventanu - Entrugar siempres - + Consiguiendo información… Ta cargando\'l conteníu solicitáu Baxa\'l ficheru del fluxu. diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index dc9e6b571..fe6dd7fe6 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -54,8 +54,6 @@ Аўдыё Фармат аўдыё па змаўчанні Фармат відэа па змаўчанні - WebM - свабодны - M4A - вышэй якасць Тэма Светлая Цёмная @@ -203,8 +201,6 @@ Аўдыё Паспрабаваць зноў Няма доступу да носьбіта - Стары плэер - Стары убудаваны плэер на Mediaframework тыс. млн. млрд. diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index ce175b28e..9a28dc625 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -156,8 +156,6 @@ Аудио Опитай отново Достъпа до хранилището е отказан - Използвай стария плейър - Използваю стария вграден Mediaframewoek плейър %s абонат diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml index d6f061888..eaf9eed9a 100644 --- a/app/src/main/res/values-bn-rBD/strings.xml +++ b/app/src/main/res/values-bn-rBD/strings.xml @@ -118,8 +118,6 @@ অডিও পুনরায় চেষ্টা করো স্টোরেজ অ্যাক্সেস করার অনুমতি অস্বীকার করা হয়েছে - পুরানো প্লেয়ার ব্যবহার করো - মিডিয়াফ্রেমওয়ার্ক প্লেয়ারের পুরানো বিল্ড। K M B diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 5ac2f2134..4285a73dc 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -216,8 +216,6 @@ S\'ha creat el directori de baixades «%1$s» Torna a intentar-ho S\'ha denegat el permís d\'accés a l\'emmagatzematge - Fes servir el reproductor antic - Antic reproductor integrat de Mediaframework Sense subscriptors Sense reproduccions diff --git a/app/src/main/res/values-cmn/strings.xml b/app/src/main/res/values-cmn/strings.xml index 858c22b23..abaaa2ed2 100644 --- a/app/src/main/res/values-cmn/strings.xml +++ b/app/src/main/res/values-cmn/strings.xml @@ -59,7 +59,6 @@ 酷黑 黑色 记住悬浮窗的尺寸与位置 - M4A — 更好的音质 记住上一次悬浮窗的位置以及大小 已清除图像缓存 最小化悬浮窗播放器 @@ -108,7 +107,6 @@ 从主视频播放器切换到其他应用时的操作 - %s 没有 最小化后台播放 - WebM — 自由视频格式 使用快速粗略定位 粗略定位功能允许播放器以略低的精确度为代价换取更快的定位速度 下载缩略图 @@ -243,8 +241,6 @@ 音频 重试 手机存储访问权限被拒绝 - 使用旧的播放器 - 旧的内置 Mediaframework 播放器 亿 diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 1e17f0621..c304d8f59 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -141,8 +141,7 @@ Toto oprávnění je vyžadováno pro otevření ve vyskakovacím okně - Použít starý přehrávač -Odstraňuje zvuk v některých rozlišeních + Odstraňuje zvuk v některých rozlišeních Zobrazovat vyšší rozlišení Pouze některá zařízení podporují přehrávání 2K/4K videí Výchozí formát videa @@ -194,8 +193,6 @@ otevření ve vyskakovacím okně Žádné výsledky Je tu sranda jak v márnici - Starý zabudovaný Mediaframework přehrávač - mld. Žádní odběratelé @@ -327,8 +324,7 @@ otevření ve vyskakovacím okně Video přehrávač Přehrávač na pozadí Přehrávač v okně - Vždy se ptát - + Získávám informace… Načítání požadovaného obsahu Stáhnout soubor streamu @@ -383,9 +379,6 @@ otevření ve vyskakovacím okně Normální písmo Větší písmo - Sledovat únik paměti - Sledování úniku paměti vypnuto -Sledování úniku paměti povoleno, aplikace může při zátěži přestat reagovat Ladění "Automaticky generováno " Povolit službu LeakCanary diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 4578040d5..621045ed9 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -120,13 +120,11 @@ Alle Kanal Deaktiviert - Alten Player benutzen Im Pop-up-Modus öffnen Bevorzugtes Videoformat Spiele im Pop-up Modus ab NewPipe-Pop-up-Modus Diese Berechtigung ist für das Öffnen im Pop-up-Modus erforderlich - Alter eingebauter Mediaframework-Player Standardauflösung des Pop-ups Höhere Auflösungen anzeigen Nur manche Geräte unterstützen das Abspielen von 2K-/4K-Videos @@ -270,7 +268,6 @@ Video-Player Hintergrund-Player Popup-Player - Immer fragen Informationen werden abgerufen… Gewünschten Inhalt laden Datenbank importieren diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 2422a493b..68e93ba58 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -29,8 +29,6 @@ Προβολή μιας επιλογής για αναπαραγωγή με το Kodi media center Ήχος Προεπιλεγμένη μορφή ήχου - WebM — δωρεάν μορφή - Μ4Α — καλύτερη ποιότητα Θέμα Σκοτεινό Φωτεινό @@ -216,8 +214,6 @@ Σύρετε για ταξινόμηση Προσπάθεια εκ νέου Δεν δώθηκε άδεια εγγραφής στην εσωτερική μνήμη - Χρήση παλαιάς συσκευής αναπαραγωγής - Παλαιά συσκευή αναπαραγωγής Mediaframework χιλ Εκ Κανένας εγγεγραμένος χρήστης diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 47748195d..e75d905df 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -268,7 +268,6 @@ abrir en modo popup Reproductor de vídeo Reproductor de fondo Reproductor de popup - Preguntar siempre Obteniendo información… Cargando contenido solicitado Importar base de datos diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index b57fdd306..a11805f3b 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -195,8 +195,6 @@ Audio Proovi uuesti Pääsuõigused salvestile puuduvad - Kasuta vana pleierit - Vana sisseehitatud mediaframework pleier K M B diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index cd0c618ee..095bbfe5e 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -117,8 +117,6 @@ Audioa Saiatu berriro Biltegia atzitzeko baimena ukatu da - Erabili erreproduzigailu zaharra - Barne Media Framework erreproduzigailu zaharra K M MM @@ -270,7 +268,6 @@ Bideo erreproduzigailua Bigarren planoko erreproduzigailua Laster-leiho erreproduzigailua - Galdetu beti Informazioa eskuratzen… Kargatzen eskatutako edukia Deskargatu jario fitxategia diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 83a076f09..fe5d74088 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -35,8 +35,6 @@ نمایش گزینه‌ای برای پخش ویدیو با مرکز رسانهٔ کودی صدا قالب صدای پیش‌گزیده - قالب آزاد — WebM - کیفیت بهتر — M4A زمینه تیره روشن @@ -213,7 +211,6 @@ جریانی برای بارگیری در دسترس نیست بدون نتیجه - استفاده از پخش‌کننده قدیمی K M B diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index ead13fb95..2a86445e0 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -148,8 +148,6 @@ Ääni Toista uudelleen Oikeus tallennustilan hallintaan evätty - Käytä vanhaa soitinta - Käytä vanhaa sisäänrakennettua Mediaframework-soitinta t. milj. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 6d8d03dec..b81195d41 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -119,8 +119,6 @@ Plus tard Désactivé Quoi :\\nRequête :\\nLangue du contenu :\\nService :\\nHeure GMT :\\nPaquet :\\nVersion :\\nVersion du système : - Utiliser l\'ancien lecteur - Ancienne version du lecteur Mediaframework K M Cette autorisation est nécessaire pour @@ -270,7 +268,6 @@ Lecteur vidéo Lecteur en arrière-plan Lecteur en fenêtré - Toujours demander Obtention des infos… Chargement du contenu Importer les données diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index f137a092e..68c831b70 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -59,8 +59,6 @@ Audio Formato de audio predeterminado Formato de vídeo predeterminado - WebM — formato libre - M4A — mellor calidade Tema Claro Escuro @@ -218,8 +216,6 @@ Audio Tentar de novo A permisión de acceso ao almacenamento foi denegada - Usar o reprodutor antigo - Versión interna anticuada do reprodutor Mediaframework K M diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 155c6a223..0f48379d5 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -149,8 +149,6 @@ שמע ניסיון חוזר הגישה לאחסון נדחתה - השתמש בנגן הישן - השתמש בנגן המובנה Mediaframework K M B diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index ed2815638..c872e41b6 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -80,8 +80,6 @@ कोडी मीडिया सेंटर के माध्यम से वीडियो चलाने के लिए एक विकल्प प्रदर्शित करें डिफ़ॉल्ट ऑडियो का फॉर्मेट डिफ़ॉल्ट विडियो का फॉर्मेट - WebM - libre फॉर्मेट - M4A - बेहतर क्वालिटी एप्प का नया रूप काला विडियो पॉपअप की आकर और उसकी स्थति को याद रखे @@ -174,8 +172,6 @@ ऑडियो फिर से कोशिश करे स्टोरेज में प्रवेश के लिए अनुमति नहीं मिली - पुराना विडियो प्लेयर प्रयोग करे - पुराना Mediaframework Player जो फ़ोन में बना हुआ है हज़ार करोड़ अरब @@ -319,9 +315,6 @@ छोटे फ़ॉंट सामांय फ़ॉंट बड़ा फ़ॉंट - मॉनिटर लीक - मेमोरी लीक मॉनिटरिंग सक्षम है, हीप डंपिंग के समय ऐप अप्रतिसादी हो सकती है - मेमोरी लीक मॉनिटरिंग अक्षम है डीबग करें ऑटो-जनरेटेड LeakCanary सक्षम करें diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 6e8595010..e7aeb924a 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -131,8 +131,6 @@ Zvuk Ponovno pokušaj Dozvola za pisanje po pohrani je odbijena - Koristi stari reproduktor - Stari ugrađeni Mediaframework reproduktor tis mil mlrd diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 24662feed..40593fdbf 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -24,8 +24,6 @@ Opció mutatása a videók Kodi médiaközponttal való lejátszására Hang Alapértelmezett hang formátum - WebM — szabad formátum - M4A — jobb minőség Letöltés Következő Nem támogatott webcím @@ -193,7 +191,6 @@ Mi:\\nKérés:\\nTartalom nyelve:\\nSzolgáltatás:\\nGMT Idő:\\nCsomag:\\nVerzió:\\nOperációs Rendszer verzió: Nincs találat - Régi lejátszó használata Adatfolyam fájl letöltése Hozzáadás @@ -236,8 +233,6 @@ Itt nincs semmi Húzza az átrendezéshez - Régi beépített Mediaframework lejátszó - e M Mrd diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index 66b901ac9..56f31eac6 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -29,8 +29,6 @@ Tampilkan opsi untuk memutar video via Kodi Audio Format audio - WebM — format bebas - M4A — kualitas lebih baik Gelap Terang Unduh @@ -124,8 +122,6 @@ \nmembuka di mode popup Mode popup NewPipe Memutar dalam mode popup - Gunakan pemutar lama - Pemutar Mediaframework bawaan versi lama Dinonaktifkan Format video Resolusi popup diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index b4bcd2926..9bccc11df 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -122,7 +122,6 @@ Modalità Popup di NewPipe Riproduzione in modalità popup Disattivato - Usa il vecchio lettore multimediale Non riproduce l\'audio con ALCUNE risoluzioni In sottofondo Popup @@ -142,8 +141,7 @@ Cancella Ridimensionamento Risoluzione migliore - Precedente lettore multimediale Mediaframework integrato - Questo permesso è necessario + Questo permesso è necessario \nper riprodurre in modalità popup Impostazioni Informazioni @@ -270,7 +268,6 @@ Lettore video Riproduzione in sottofondo Riproduzione in modalità popup - Chiedi sempre Raccogliendo informazioni… Caricamento del contenuto richiesto Importa database diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index e1e76475e..38acee61d 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -123,8 +123,6 @@ このアクセス許可が必要です NewPipe ポップアップモード ポップアップモードで再生中 - 古いプレーヤーを使用する - 古い内蔵のMediaframeworkプレーヤー 無効 デフォルトの動画形式 デフォルトのポップアップ解像度 @@ -253,7 +251,6 @@ 動画プレーヤー バックグラウンドプレーヤー ポップアッププレーヤー - 常に尋ねる 情報を取得しています… コンテンツを読み込んでいます 動画ファイルをダウンロード diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index bcaafbcfc..a3f25b033 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -173,9 +173,6 @@ 결과 없음 구독할 항목을 추가하세요 - 구형 플레이어 사용 - 내장된 구형 Mediaframework 플레이어 사용 - 백만 10억 diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 4ffcd26d6..4b21a57cd 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -120,8 +120,6 @@ Vaizdas Muzika Bandyti iš naujo - Naudoti seną grotuvą - Senas įtaisytas media grotuvas %s prenumeratorius diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 47413c6c3..fc941d0db 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -202,8 +202,6 @@ Звук Пробај повторно Нема привилегии за пристап - Користи го стариот плеер - Користи го стариот Mediaframework плеер K M diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index baaa48ca8..a806df31a 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -141,8 +141,6 @@ Senere Avskrudd - Bruk gammel avspiller - K M @@ -153,8 +151,7 @@ reCAPTCHA-oppgave forespurt - Gammel innebygd Mediaframework-avspiller -Fjerner lyd ved NOEN oppløsninger + Fjerner lyd ved NOEN oppløsninger Abonner Abonnert Kanalabonnent oppsagt @@ -315,8 +312,7 @@ Videoavspiller Bakgrunnsavspiller Oppsprettsavspiller - Spør alltid - + Henter informasjon… Laster forespurt innhold Importer database diff --git a/app/src/main/res/values-nl-rBE/strings.xml b/app/src/main/res/values-nl-rBE/strings.xml index 5fb175b3e..51a30aa36 100644 --- a/app/src/main/res/values-nl-rBE/strings.xml +++ b/app/src/main/res/values-nl-rBE/strings.xml @@ -189,8 +189,6 @@ Geluid Opnieuw proberen Toegang tot opslag geweigerd - Gebruik oude speler - Verouderden ingebouwde Mediaframework-speler K M B diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 3bf219912..04b47c4c8 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -123,8 +123,6 @@ \nopenen in pop-upmodus NewPipe-pop-upmodus Speelt af in pop-upmodus - Oude speler gebruiken - Verouderde ingebouwde Mediaframework-speler Standaard videoformaat Uitgeschakeld Standaardresolutie van pop-up @@ -270,7 +268,6 @@ Videospeler Achtergrondspeler Pop-upspeler - Altijd vragen Bezig met ophalen van informatie… Bezig met laden van gevraagde inhoud Databank importeren diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index c33bfde1b..de26a1638 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -125,8 +125,6 @@ Preferovaný formát videa Prehrávanie v mini okne Vypnuté - Použiť starší prehrávač - Starý zabudovaný prehrávač Mediaframework Predvolená veľkosť mini okna Zobraziť vyššie rozlíšenie Len niektoré zariadenia podporujú videá 2K/4K diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index ec2a0bea5..4f604ae5b 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -153,8 +153,6 @@ odpiranje v pojavnem načinu Predvajanje v pojavnem načinu Onemogočeno - Uporabi star predvajalnik - Zastarela različica predvajalnika Mediaframework Prednostni zapis video datoteke Privzeta ločljivost pojavnega okna Pokaži večje ločljivosti diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 5182144e0..a34cc9e11 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -124,7 +124,6 @@ Режим прозорчета Пуштање у режиму прозорчета Искључено - Користи стари плејер Пожељни формат видеа Резолуција искачућег прозора Прикажи више резолуције @@ -143,7 +142,6 @@ Приказује предлоге током претраге Искачући прозор Мењање величине - Стара градња Медијафрејмворк плејера Претплаћен Претплати Главно diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 28c84fc9e..7aa021182 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -51,7 +51,6 @@ ஒலி முதல் ஒலி வடிவம் முதல் காணிலி வடிவம் - M4A - மேம்பட்ட தரம் வார்ப்புரு வெளிர் அடர் diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml index 5915aee7a..ff9969abc 100644 --- a/app/src/main/res/values-te/strings.xml +++ b/app/src/main/res/values-te/strings.xml @@ -102,7 +102,6 @@ ఆడియో మళ్ళీ ప్రయత్నించు నిల్వ అనుమతి తిరస్కరించబడింది - పాత ఆటగాడు ఉపయోగించండి కె ఎం బి diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index b7c4b0156..5209fe250 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -120,8 +120,6 @@ Yorumunuz (İngilizce): Ayrıntılar: Video ön izleme küçük resmi - Eski oynatıcıyı kullan - Eski içe gömülü Mediaframework oynatıcısı B M MR @@ -270,7 +268,6 @@ Video oynatıcı Arka plan oynatıcı Açılır oynatıcı - Her zaman sor Bilgi alınıyor… İstenen içerik yükleniyor Veri tabanını içe aktar diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index ffc996f49..6eca64d34 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -90,7 +90,6 @@ Відео Аудіо Повторити спробу - Використовувати старий програвач тис. млн. млрд. @@ -245,7 +244,7 @@ Вільне та легке потокове програвання на Android. Які б не були Ваші ідеї: переклад, дизайн, легкий чи глобальний рефакторинг - будь-яка допомога завжди у нагоді. Що більше зроблено, то ліпшим стає NewPipe! Налагодження - Нічого немає... чути лише цвіркунів + Нічого немає… чути лише цвіркунів Немає переглядів %s перегляд diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index af23f383c..b1899622f 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -113,8 +113,6 @@ Âm thanh Thử lại Quyền truy cập bộ nhớ đã bị từ chối - Sử dụng trình phát cũ - Máy nghe nhạc Mediaframework tích hợp sẵn ngàn triệu tỉ diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 55cfad9ef..8d5701a4f 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -175,8 +175,6 @@ NewPipe 通知 NewPipe 后台播放和窗口播放器的通知 - 使用旧版播放器 - 旧版本的内置媒体播放器 K M B diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index d1b5fde31..3b38daca6 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -119,7 +119,6 @@ 無法取得圖片 應用程式或介面出現問題 事件:\\n請求:\\n內容語言:\\n服務:\\nGMT 時間:\\nPackage:\\n版本:\\n作業系統版本: - 使用舊播放器 K M reCAPTCHA @@ -142,7 +141,6 @@ 清除 最佳解像度 調整大小 - 使用舊的內置 Mediaframework 播放器 B 關於 NewPipe 設定 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 655cd0dd6..441061657 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -115,8 +115,6 @@ 音訊 重試 無法存取儲存空間 - 使用舊式播放器 - 舊型內建 Mediaframework 播放器 百萬 十億 @@ -275,7 +273,6 @@ 影片播放器 背景播放器 懸浮視窗播放器 - 總是詢問 正在取得資訊… 正在載入要求的內容 下載串流檔案 @@ -315,9 +312,6 @@ 正常字體 加大字體 某些東西即將在此出現 ;D - 監測流失 - 啟用記憶體流失監測技術,當 heap dump 時,應用程式可能會反應遲鈍 - 已停用記憶體流失監測 除錯 自動產生 啟用 LeakCanary From 93edb333d405ab1104dcbd21ff033c5fbc49701a Mon Sep 17 00:00:00 2001 From: Karel S Date: Wed, 29 May 2019 09:50:35 +0000 Subject: [PATCH 109/138] Translated using Weblate (Czech) Currently translated at 90.3% (400 of 443 strings) --- app/src/main/res/values-cs/strings.xml | 132 ++++++------------------- 1 file changed, 31 insertions(+), 101 deletions(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 1e17f0621..1a82d2f0d 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -15,10 +15,8 @@ otočení Použít externí video přehrávač Použít externí audio přehrávač - Stažené audio je uloženo zde Zadejte umístění pro stažené audio soubory - Složka pro stažené audio Výchozí rozlišení Přehrát pomocí Kodi @@ -27,7 +25,6 @@ Umístění pro stažené video Cesta, kam se uloží stažené video Zadejte umístění pro stažená videa - Zobrazit možnost \"Přehrát pomocí Kodi\" Zobrazit možnost přehrání videa pomocí multimediálního centra Kodi Zvuk @@ -35,7 +32,6 @@ Téma Tmavé Světlé - Stáhnout Další videa Zobrazovat \'další\' a \'podobná\' videa @@ -53,7 +49,6 @@ Nebylo možné analyzovat stránku Obsah není k dispozici Obsah blokuje GEMA - Náhled videa Náhled videa Náhled avataru uploadera @@ -61,7 +56,6 @@ To se mi nelíbí Použít Tor (Experimentální) Vynutit stahování skrz Tor pro zvýšené soukromí (streamovaná videa zatím nepodporována). - Nebylo možné vytvořit složku pro stažené soubory \'%1$s\' Vytvořena složka pro stažené soubory \'%1$s\' Automaticky přehrávat @@ -70,7 +64,6 @@ Věkově omezený obsah Zobrazit video s věkovým omezením. Povolit tento obsah lze v \"Nastavení\". ŽIVĚ - Nebylo možné kompletně analyzovat stránku Začni stiskem hledat Zkopírováno do schránky @@ -85,15 +78,12 @@ Vlákna Zastavit Smazat - Start Zkusit znovu Video Zvuk Nahlásit chybu Podrobnosti: - - Co se stalo: NAHLÁSIT Omlouváme se, tohle se nemělo stát. @@ -105,7 +95,6 @@ Nepodařilo se dostat žádný stream Nepodařilo se nastavit menu stahování Nahlásit chybu - Stažené soubory Stažené soubory Info: @@ -114,35 +103,25 @@ Přehrát Nová mise OK - reCAPTCHA Výzva reCAPTCHA Požadována výzva reCAPTCHA - Černé - Kontrolní součet - Prosím vyberte dostupnou složku pro stahování - Hlášení uživatele - Co:\\nŽádost:\\nJazyk obsahu:\\nSlužba:\\nČas GMT:\\nBalíček:\\nVerze:\\nVerze OS: Vše Kanál Ano Později - - tis. - Otevřít ve vyskakovacím okně mil. Toto oprávnění je vyžadováno pro otevření ve vyskakovacím okně - Použít starý přehrávač -Odstraňuje zvuk v některých rozlišeních + Odstraňuje zvuk v některých rozlišeních Zobrazovat vyšší rozlišení Pouze některá zařízení podporují přehrávání 2K/4K videí Výchozí formát videa @@ -154,15 +133,11 @@ otevření ve vyskakovacím okně Odběr zrušen Nelze změnit odběr Nelze aktualizovat odběr - Hlavní Odběry - Co je nového - Na pozadí V okně - Výchozí rozlišení vyskakovacího okna Ovládání přehrávače gesty Používat gesta pro nastavení jasu a hlasitosti přehrávače @@ -187,46 +162,36 @@ otevření ve vyskakovacím okně Změna velikosti Nejlepší rozlišení Vrátit - NewPipe notifikace Notifikace pro NewPipe přehrávače v pozadí a v okně - Žádné výsledky Je tu sranda jak v márnici - Starý zabudovaný Mediaframework přehrávač - mld. - Žádní odběratelé - %s odběratel - %s odběratelé - %s odběratelů - - + %s odběratel + %s odběratelé + %s odběratelů + Žádná zhlédnutí - %s zhlédnutí - %s zhlédnutí - %s zhlédnutí - - + %s zhlédnutí + %s zhlédnutí + %s zhlédnutí + Žádná videa - %s video - %s videa - %s videí - - + %s video + %s videa + %s videí + Stahování Povolené znaky v názvech souborů Neplatné znaky budou nahrazeny těmito Náhradní znak - Písmena a číslice Většina speciálních znaků - O NewPipe Nastavení O aplikaci @@ -243,7 +208,6 @@ otevření ve vyskakovacím okně Pokud máte nápady na zlepšení jako; překlad, změny designu, vylepšování kódu nebo opravdu velké změny kódu - pomoc je vždy vítána. Čím více se udělá, tím lepší to bude! Přečíst licenci Podílet se - Historie Vyhledáváno Zhlédnuto @@ -252,18 +216,15 @@ otevření ve vyskakovacím okně Historie je prázdná Historie vymazána Položka byla odstraněna -Zobrazovat tip \"Podržet pro přidání\" + Zobrazovat tip \"Podržet pro přidání\" Zobrazí se po stisku tlačítek přehrát na pozadí nebo přehrát v okně na stránce s videem Ve frontě přehrávače na pozadí Ve frontě přehrávače v okně Přehrát vše - Tento stream nelze přehrát Došlo k neobnovitelné chybě přehrávače Obnovování z chyby přehrávače - Odstranit tuto položku z historie vyhledávání? - Obsah úvodní obrazovky Prázdná stránka Kiosek @@ -273,7 +234,6 @@ otevření ve vyskakovacím okně Zvolte kanál Žádný kanál dosud neodebírán Zvolte kiosek - Kiosek Trendy Top 50 @@ -284,14 +244,13 @@ otevření ve vyskakovacím okně Podrobnosti Nastavení zvuku Podrž pro zařazení do fronty -[Neznámý] - + [Neznámý] Do fronty na pozadí Do fronty v okně Začne hrát zde Začne zde, když na pozadí Začne zde v okně -Donate + Donate NewPipe je vyvíjen dobrovolníky, kteří tráví svůj čas, aby vaše zkušenost s aplikací byla co nejlepší. Vraťte vývojářům něco zpět, aby mohli NewPipe dále zlepšovat a zároveň si vychutnat šálek kávy. Daruj Webová stránka @@ -302,13 +261,11 @@ otevření ve vyskakovacím okně Na pozadí Do okna Přepnout na hlavní - Otevřít Drawer Zavřít Drawer Nenalezen přehrávač streamu (pro přehrání můžete nainstalovat např. VLC). Vždy Pouze jednou - Importovat databázi Exportovat databázi Přepíše vaši dosavadní historii a odběry @@ -317,87 +274,67 @@ otevření ve vyskakovacím okně Neplatná URL Nenalezeny žádné video streamy Nenalezeny žádné audio streamy - Exportováno Importováno Žádný platný soubor ZIP Upozornění: Nelze importovat všechny soubory. Tímto se anuluje vaše aktuální nastavení. - Video přehrávač Přehrávač na pozadí Přehrávač v okně Vždy se ptát - Získávám informace… Načítání požadovaného obsahu -Stáhnout soubor streamu + Stáhnout soubor streamu Ukázat informace - Uložené playlisty - Přidat do - Táhnout pro přeskupení - Vytvořit Smazat jeden Smazat vše Zahodit Přejmenovat - Přejete si smazat tuto položku z historie shlédnutí? Jste si jisti, že chcete odstranit všechny položky z historie? Poslední přehráno Nejvíce přehráno - Zde se brzy něco objeví ;D - - Vždy se zeptat - Nový playlist Vymazat Přejmenovat Jméno Přidat do playlistu Nastavit jako náhled playlistu - Založit playlist Smazat záložku - Smazat tento playlist\? Playlist vytvořen V playlistu Náhled playlistu změněn. Playlist nelze smazat. - - Žádné titulky - + Bez titulků Přizpůsobit Vyplnit Zvětšit - Velikost písma nadpisu Menší písmo Normální písmo Větší písmo - Sledovat únik paměti Sledování úniku paměti vypnuto -Sledování úniku paměti povoleno, aplikace může při zátěži přestat reagovat + Sledování úniku paměti povoleno, aplikace může při zátěži přestat reagovat Ladění "Automaticky generováno " Povolit službu LeakCanary Monitoring úniku paměti může způsobit nereagování aplikace při heap dumpingu - Nahlásit mimo-cyklické chyby Vynutit vykazování výjimek Rx mimo fragment nebo životnost cyklu po odstranění - -Použít rychlé nepřesné hledání + Použít rychlé nepřesné hledání Nepřesné hledání umožní přehrávači posouvat se rychleji, ale se sníženou přesností Načítat náhledy - Po vypnutí se nestahují náhledy a tím se šetří data a využití paměti. Změna tohoto nastavení vyčistí mezipamět obrázků. + Vypnout, aby se zabránilo načítání náhledů a tím se ušetřily data a používání paměti. Změna tohoto nastavení vyčistí mezipamět obrázků v paměti i na disku. Mezipaměť obrázků vymazána Vymazat metadata v mezipaměti Odebrat všechna data uložená v mezipaměti @@ -405,13 +342,11 @@ otevření ve vyskakovacím okně Automatická fronta dalšího streamu Automaticky připojí související stream při přehrávání posledního streamu v neopakující se frontě. Soubor - Neexistující složka Neexistující zdroj souboru/obsahu Soubor neexistuje nebo chybí oprávnění k jeho čtení či zápisu Název souboru nesmí být prázdný Došlo k chybě: %1$s - Import/export \n Importovat @@ -419,16 +354,12 @@ otevření ve vyskakovacím okně Importovat z \n Exportovat do - Importuji… Exportuji… - Import souboru Předchozí export - Odběry nelze importovat Odběry nelze exportovat - Importovat YouTube odběry stáhnutím exportního souboru: \n \n1. Přejděte na tuto URL adresu: %1$s @@ -441,27 +372,21 @@ otevření ve vyskakovacím okně \n3. Na vyžádání se přihlašte \n4. Zkopírujte URL adresu profilu, na kterou jste byli přesměrováni. vašeID, soundcloud.com/yourid - Pamatujte, že tato operace může být náročná na data. \n \nChcete pokračovat? - Ovládání rychlosti přehrávání Rychlost Stupeň tónu Rozpojit (může způsobit zkreslení) Nightcore mód Výchozí nastavení -Ke stažení nejsou dostupné žádné streamy - + Ke stažení nejsou dostupné žádné streamy Preferovaná \'otevřít\' akce Výchozí chování při otevírání obsahu — %s - Titulky Upravuje velikost textu titulků a styly pozadí. Změny se projeví po restartu aplikace. - K přehrání tohoto souboru chybí vhodná aplikace - Vymazat historii sledování Vymaže historii přehrávaných streamů Vymazat celkovou historii sledování\? @@ -471,7 +396,6 @@ otevření ve vyskakovacím okně Vymazat celkovou historii vyhledávání\? Historie vyhledávání smazána. Jedna položka smazána. - NewPipe je copyleft libre software: Můžete jej používat, sdílet a vylepšovat dle vaší vůle. Redistribuovat a/nebo upravovat lze za podmínek GNU General Public Licence zveřejňované nadací Free Software Foundation, a to buď za podmínek licence verze 3 nebo (dle vaší volby) jakékoli pozdější verze. kanály Playlisty @@ -482,16 +406,13 @@ otevření ve vyskakovacím okně \nZásady ochrany soukromí NewPipe podrobně vysvětlují, jaké údaje jsou odesílány a ukládány, když odešlete zprávu o pádu aplikace. Přečíst zásady ochrany soukromí Chcete také přenést nastavení? - Zrychleně vpřed během ticha Krok Reset - Abychom vyhověli Obecnému nařízení o ochraně osobních údajů (GDPR), upozorňujeme vás na zásady ochrany soukromí v NewPipe. Přečtěte si je prosím pozorně. \nJe potřeba je odsouhlasit, abyste nám mohli odeslat hlášení chyb. Přijmout Odmítnout - Bez omezení Omezit rozlišení při použití mobilních dat Minimalizovat při přepínání aplikací @@ -511,4 +432,13 @@ otevření ve vyskakovacím okně Notifikace aktualizace aplikace Notifikace pro novou verzi NewPipe Externí úložiště není k dispozici + Chyba při načítání uložených karet, použijí se výchozí karty + Obnovit do výchozího nastavení + Chcete obnovit výchozí nastavení\? + Počet odběratelů není k dispozici + Karty, které jsou zobrazeny na hlavní stránce + Výběr + Aktualizace + Události + Konference \ No newline at end of file From 9395df4cc36463fb8f46946fc70d1987c987ab8d Mon Sep 17 00:00:00 2001 From: Cipisek Rumcajsu Date: Wed, 29 May 2019 10:08:28 +0000 Subject: [PATCH 110/138] Translated using Weblate (Czech) Currently translated at 90.3% (400 of 443 strings) --- app/src/main/res/values-cs/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 1a82d2f0d..a80fef606 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -307,7 +307,7 @@ otevření ve vyskakovacím okně Jméno Přidat do playlistu Nastavit jako náhled playlistu - Založit playlist + Přidat playlist do záložek Smazat záložku Smazat tento playlist\? Playlist vytvořen @@ -377,7 +377,7 @@ otevření ve vyskakovacím okně \nChcete pokračovat? Ovládání rychlosti přehrávání Rychlost - Stupeň tónu + Výška tónu Rozpojit (může způsobit zkreslení) Nightcore mód Výchozí nastavení From 359a9a96d6e927a008a92a5f69ca728fe3d42560 Mon Sep 17 00:00:00 2001 From: Karel S Date: Thu, 30 May 2019 06:32:35 +0000 Subject: [PATCH 111/138] Translated using Weblate (Czech) Currently translated at 99.8% (442 of 443 strings) --- app/src/main/res/values-cs/strings.xml | 42 ++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index a80fef606..ce2943fc4 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -441,4 +441,46 @@ otevření ve vyskakovacím okně Aktualizace Události Konference + Zobrazit oznámení s výzvou k aktualizaci aplikace, když je k dispozici nová verze + Režim zobrazení seznamu + Seznam + Mřížka + Automaticky + Přepnout zobrazení + K dispozici je aktualizace aplikace NewPipe! + Klepněte pro stažení + Hotovo + Ve frontě + Pozastaveno + ve frontě + post-processing + Fronta + Akce odmítnuta systémem + Stahování se nezdařilo + Stahování dokončeno + % s stahování dokončeno + Vytvořit jedinečný název + Přepsat + Stažený soubor s tímto názvem již existuje + Stahování s tímto názvem již probíhá + Zobrazit chybu + Kód + Soubor nelze vytvořit + Cílovou složku nelze vytvořit + Oprávnění odepřeno systémem + Zabezpečené připojení selhalo + Server se nepodařilo najít + Nelze se připojit k serveru + Server neposílá data + Server neakceptuje vícevláknové stahování, opakujte akci s @string/msg_threads = 1 + Požadovaný rozsah nelze splnit + Nenalezeno + Post-processing selhal + Vyčistit dokončená stahování + Pokračovat ve stahování %s souborů, čekajících na stažení + Zastavit + Maximální počet pokusů o opakování + Maximální počet pokusů před zrušením stahování + Pozastavit při přepnutí na mobilní data + Stahování, která nelze pozastavit, budou restartována \ No newline at end of file From e1ead9d2efb7c974c3cca9618ca4df830f6d87f6 Mon Sep 17 00:00:00 2001 From: mohammadmdp Date: Fri, 31 May 2019 00:20:13 +0000 Subject: [PATCH 112/138] Translated using Weblate (Persian) Currently translated at 58.9% (261 of 443 strings) --- app/src/main/res/values-fa/strings.xml | 69 +++++++------------------- 1 file changed, 18 insertions(+), 51 deletions(-) diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 83a076f09..f97a85463 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -17,15 +17,12 @@ چرخش استفاده از پخش‌کنندهٔ ویدیوی خارجی استفاده از پخش‌کنندهٔ صدای خارجی - مسیر بارگیری ویدیو مسیر ذخیرهٔ ویدیوهای بارگیری شده مسیر بارگیری را برای ویدیوها وارد کنید - پوشه بارگیری صدا صدای بارگیری شده در این‌جا نگه داشته می‌شود مسیر بارگیری را برای صداها وارد کنید - پخش خودکار هنگامی که نیوپایپ از کارهٔ دیگری فراخوانی می‌شود، ویدیوی به طور خودکار پخش شود وضوح پیش‌گزیده @@ -40,7 +37,6 @@ زمینه تیره روشن - بارگیری بعدی نماش ویدیوهای «بعدی» و «مشابه» @@ -58,7 +54,6 @@ بارگیری‌ها بارگیری‌ها گزارش خطا - خطا خطای شبکه نمی‌توان تمام بندانگشتی‌ها را بار کرد @@ -78,8 +73,6 @@ چه روی داد: توضیح شما (به انگلیسی): جزییات: - - بندانگشتی پیش‌نمایش ویدیو بندانگشتی پیش‌نمایش ویدیو بندانگشتی کاربر بارگذار @@ -89,24 +82,19 @@ (آزمایشی) اجبار ترافیک بارگیری از مسیر تور برای محرمانگی بیش‌تر (هنوز پخش جریانی پشتیبانی نمی‌شود). گزارش یک خطا گزارش کاربر - نمی‌توان شاخهٔ بارگیری «%1$s» را ایجاد کرد شاخهٔ بارگیری «%1$s» ایجاد شد - ویدیو صدا تلاش دوباره اجازهٔ دسترسی به انبار ذخیره رد شد - شروع مکث نمایش حذف مجموع مقابله‌ای - مآموریت جدید قبول - نام پرونده رشته‌ها خطا @@ -118,8 +106,7 @@ لطفاً صبر کنید… در حافظه رونوشت شد. لطفاً یک شاخهٔ بارگیری موجود را برگزینید. - -هیچ پخش کننده جریانی پیدا نشد (شما می‌توانید برنامه وی‌ال‌سی را برای پخش آن نصب کنید). + هیچ پخش کننده جریانی پیدا نشد (شما می‌توانید برنامه وی‌ال‌سی را برای پخش آن نصب کنید). بارگیری پرونده جریان حذف صدا در برخی کیفیت‌ها اشتراک @@ -128,16 +115,12 @@ ناتوانی در تغییر وضعیت اشتراک ناتوانی در به‌روزرسانی اشتراک نمایش اطلاعات - اصلی اشتراک‌ها فهرست‌های پخش دارای نشانک - موارد جدید - پس زمینه افزودن به - نمایش کیفیت بالاتر تنها برخی دستگاه‌ها توانایی پخش ویدئوهای 2K و 4K را دارند قالب ویدئوی پیش‌فرض @@ -168,10 +151,8 @@ همیشه فقط یک‌بار پرونده - اعلان نیوپایپ [ناشناخته] - وارد کردن پایگاه‌داده "صادرکردن " تاریخچه و اشتراک‌های شما بازنویسی خواهند شد @@ -181,17 +162,13 @@ وارد کردن وارد کردن از صادر کردن به - در حال وارد کردن… در حال صدور… - ناتوانی در ورود اشتراک‌ها ناتوانی در صدور اشتراک‌ها - کنترل‌های سرعت پخش قبول رد - بدون محدودیت هیچ بهترین وضوح @@ -211,47 +188,37 @@ نام پرونده نمی‌تواند خالی باشد خطایی رخ داد: %1$s جریانی برای بارگیری در دسترس نیست - بدون نتیجه استفاده از پخش‌کننده قدیمی K M B - - %s مشترک - %s مشترک - - + %s مشترک + %s مشترک + بدون بازدید - %s بازدید - %s بازدید - - + %s بازدید + %s بازدید +
بدون ویدئو - ویدئو - ویدئو - - + ویدئو + ویدئو + ایجاد پاک کردن یک مورد پاک‌کردن همه صرف نظر تغییر نام - یک مورد پاک شد. - نویسه‌های مجاز در نام پرونده‌ها نویسه‌های نامعتبر با این مقدار جایگزین شدند نویسه جایگزین - حروف و اعداد مهم‌ترین نویسه‌های خاص - کاره‌ای برای پخش این پرونده نصب نشده است - درباره نیوپایپ تنظیمات درباره @@ -269,8 +236,6 @@ خواندن سیاست حریم خصوصی پروانه نیوپایپ خواندن پروانه - - تاریخچه جستجو شده دیده‌شده @@ -284,7 +249,6 @@ می‌خواهید همه موارد را از تاریخچه پاک کنید؟ آخرین پخش‌شده بیشترین پخش‌شده - محتوای صفحه اصلی صفحه خالی صفحه کیوسک @@ -305,25 +269,20 @@ پخش‌کننده ویدئو پخش‌کننده پس‌زمینه همیشه بپرس - در حال دریافت اطلاعات… بارگذری محتوای درخواستی - فهرست پخش جدید پاک‌کردن تغییر نام نام افزودن به فهرست پخش استفاده به عنوان تصویر فهرست پخش - این فهرست پخش پاک شود؟ فهرست پخش ایجاد شد به فهرست پخش افزوده شد تصویر فهرست پخش تغییر کرد. ناتوانی در پاک‌کردن فهرست پخش. - بدون توضیحات - توضحیات لغو اشتراک زبان جدید @@ -332,4 +291,12 @@ تاریخچه و حافظه نهان اشکال‌زدایی به‌روزرسانی‌ها + در پنجره جداگانه باز شود + حالت پنجره مجزا + اندازه پیش فرض پنجره جداگانه + پنجره جداگانه + به یاد داشتن اندازه و موقعیت پنجره جداگانه + به یاد داشتن آخرین اندازه و موقعیت قبلی پنجره جداگانه + زمان فعلی پخش کننده را به صورت تقریبی و سریع جلو ببر + این گزینه باعث می شود هنگام جلو/عقب کردن زمان تصویر، به جای زمان دقیق انتخاب شده، به زمان غیر دقیق و نزدیک به مکان انتخاب شده برود که این کار سریع تر انجام می شود \ No newline at end of file From 1a9922d790d98ce01781baf1af3b0c17ed10a086 Mon Sep 17 00:00:00 2001 From: ssantos Date: Sat, 1 Jun 2019 10:16:39 +0000 Subject: [PATCH 113/138] Translated using Weblate (German) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-de/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 4578040d5..52cca4a3d 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -351,7 +351,7 @@ Standard Abschalten, um das Laden von Miniaturansichten zu verhindern, was Daten- und Speicherverbrauch spart. Änderungen löschen den Bildzwischenspeicher sowohl im Arbeitsspeicher als auch auf dem internen Speicher. Nächsten Stream automatisch einreihen - Automatisches Anhängen eines verwandten Streams beim Abspielen des letzten Streams in einer nicht wiederholten Warteschlange. + Automatisches Anhängen eines verwandten Streams beim Abspielen des letzten Streams in einer nicht wiederholten Warteschlange Hier wird bald etwas stehen ;D Wiedergabeliste mit Lesezeichen versehen Anpassen From f766f383ea30bb17825de670a08564197a6b44fe Mon Sep 17 00:00:00 2001 From: yunna Date: Sat, 1 Jun 2019 09:18:13 +0000 Subject: [PATCH 114/138] Translated using Weblate (English) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values/strings.xml | 83 +++-------------------------- 1 file changed, 6 insertions(+), 77 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2d12f6ad6..b59a6079a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -32,27 +32,21 @@ Could not change subscription Could not update subscription Show info - Main Subscriptions Bookmarked Playlists New Tab Choose Tab - What\'s New - Background Popup Add To - Video download path Path to store downloaded videos in Enter download path for videos - Audio download folder Downloaded audio is stored here Enter download path for audio files - Autoplay Plays a video when NewPipe is called from another app Default resolution @@ -82,7 +76,7 @@ Remove all cached webpage data Metadata cache wiped Auto-queue next stream - Auto-append a related stream when playing the last stream in a non-repeating queue. + Auto-append a related stream when playing the last stream in a non-repeating queue Volume gesture control Use gestures to control the volume of the player Brightness gesture control @@ -152,22 +146,17 @@ Always Just Once File - newpipe NewPipe Notification Notifications for NewPipe background and popup players - newpipeAppUpdate App Update Notification Notifications for new NewPipe version - [Unknown] - Toggle Orientation Switch to Background Switch to Popup Switch to Main - Import database Export database Overrides your current history and subscriptions @@ -212,7 +201,6 @@ Using default tabs, error while reading saved tabs Restore defaults Do you want to restore the defaults? - Sorry, that should not have happened. Guru Meditation. @@ -224,8 +212,6 @@ What:\\nRequest:\\nContent Lang:\\nService:\\nGMT Time:\\nPackage:\\nVersion:\\nOS version: Your comment (in English): Details: - - Video preview thumbnail Video preview thumbnail @@ -240,19 +226,15 @@ @string/no_videos Nothing here but crickets Drag to reorder - Cannot create download directory \'%1$s\' Created download directory \'%1$s\' - Video Audio Retry Storage access permission denied - K M B - No subscribers @@ -260,19 +242,16 @@ %s subscribers Subscribers count not available - No views %s view %s views - No videos - Video - Videos - - + Video + Videos + Start Pause @@ -284,11 +263,9 @@ Checksum Dismiss Rename - New mission OK - Filename Threads @@ -303,29 +280,23 @@ Please select an available download folder This permission is needed to\nopen in popup mode 1 item deleted. - MD5 SHA-1 reCAPTCHA reCAPTCHA challenge reCAPTCHA challenge requested - - Download Allowed characters in filenames Invalid characters are replaced with this value Replacement character - [^\\w\\d]+ [\\n\\r|\\?*<":>/']+ Letters and digits Most special characters - No app installed to play this file - About NewPipe Settings @@ -356,8 +327,6 @@ NewPipe\'s License NewPipe is copyleft libre software: You can use, study share and improve it at will. Specifically you can redistribute and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Read license - - History Searched @@ -372,7 +341,6 @@ Are you sure you want to delete all items from history? Last Played Most Played - Content of main page What tabs are shown on the main page @@ -391,7 +359,6 @@ Warning: Could not import all files. This will override your current setup. Do you want to also import settings? - Kiosk Trending @@ -399,7 +366,6 @@ New & hot Conferences %1$s/%2$s - Background player Popup player @@ -412,28 +378,22 @@ Start playing here Start here when backgrounded Start here on new popup - Open Drawer Close Drawer YouTube SoundCloud Something will appear here soon ;D - - NewPipe Preferred \'open\' action Default action when opening content — %s - Video player Background player Popup player Always ask - Getting info… "Loading requested content" - New Playlist Delete @@ -441,57 +401,42 @@ Name Add To Playlist Set as Playlist Thumbnail - Bookmark Playlist Remove Bookmark - Delete this playlist\? Playlist created Playlisted Playlist thumbnail changed. Could not delete playlist. - No Captions - Fit Fill Zoom - Auto-generated - Captions Modify player caption text scale and background styles. Requires app restart to take effect. - Enable LeakCanary Memory leak monitoring may cause the app to become unresponsive when heap dumping - Report out-of-lifecycle errors Force reporting of undeliverable Rx exceptions outside of fragment or activity lifecycle after disposal - Import/export Import Import from Export to - Importing… Exporting… - Import file Previous export - Could not import subscriptions Could not export subscriptions - Import YouTube subscriptions by downloading the export file:\n\n1. Go to this URL: %1$s\n2. Log in when asked\n3. A download should start (that\'s the export file) Import a SoundCloud profile by typing either the URL or your ID:\n\n1. Enable \"desktop mode\" in a web-browser (the site is not available for mobile devices)\n2. Go to this URL: %1$s\n3. Log in when asked\n4. Copy the profile URL you were redirected to. yourID, soundcloud.com/yourid - Keep in mind this operation can be network expensive.\n\nDo you want to continue? - Playback Speed Controls Tempo @@ -500,12 +445,10 @@ Fast-forward during silence Step Reset - In order to comply with the European General Data Protection Regulation (GDPR), we herby draw your attention to NewPipe\'s privacy policy. Please read it carefully.\nYou must accept it to send us the bug report. Accept Decline - No limit Limit resolution when using mobile data @@ -521,51 +464,39 @@ 240p 144p - Updates Show a notification to prompt app update when a new version is available - Minimize on app switch Action when switching to other app from main video player — %s None Minimize to background player Minimize to popup player - List view mode + List view mode List Grid - Auto + Auto Switch View - NewPipe Update Available! Tap to download - - - Finished In queue - paused queued post-processing - Queue - Action denied by the system - Download failed Download finished %s downloads finished - Generate unique name Overwrite A downloaded file with this name already exists There is a download in progress with this name - Show error Code @@ -580,7 +511,6 @@ Requested range not satisfiable Not found Post-processing failed - Clear finished downloads Continue your %s pending transfers from Downloads Stop @@ -588,5 +518,4 @@ Maximum number of attempts before canceling the download Pause on switching to mobile data Downloads that can not be paused will be restarted - \ No newline at end of file From bd42f4188f2792845ca8d7942f01aada11caa017 Mon Sep 17 00:00:00 2001 From: yunna Date: Sat, 1 Jun 2019 09:16:39 +0000 Subject: [PATCH 115/138] Translated using Weblate (Japanese) Currently translated at 99.8% (442 of 443 strings) --- app/src/main/res/values-ja/strings.xml | 62 +++++++++++++------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index e1e76475e..d4a02c8e6 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -3,7 +3,7 @@ %1$s に公開 動画プレイヤーが見つかりません。VLC を入手しますか? 入手 - 取り消し + キャンセル ブラウザで開く 共有 保存 @@ -47,7 +47,7 @@ 外観 その他 ネットワークエラー - 音楽を保存する場所 + 音声を保存する場所 ダウンロードした音楽をここに保存します 音楽ファイルをダウンロードする場所を入力して下さい。 保存場所 \'%1$s\' を作成できません @@ -61,7 +61,7 @@ 保存メニューを設定できませんでした 生放送にはまだ対応していません コンテンツ - 年齢制限のあるコンテンツ + 年齢制限のあるコンテンツを表示する 年齢制限された動画を表示しています。設定から許可することができます。 ウェブサイトを完全には解析できませんでした 動画を取得できませんでした @@ -86,7 +86,7 @@ 開始 一時停止 再生 - 削除 + 削除する チェックサム 新しいミッション OK @@ -120,7 +120,7 @@ 後で ポップアップモードで開く ポップアップモードで開くには -このアクセス許可が必要です +\n権限の許可が必要です NewPipe ポップアップモード ポップアップモードで再生中 古いプレーヤーを使用する @@ -135,14 +135,14 @@ フィルター 更新 クリア - ポップアップのサイズと位置を記憶する - 前回ポップアップしたサイズと位置を記憶する + ポップアップのサイズと位置を記憶 + ポップアップしたサイズと位置を記憶します ポップアップ サイズを変更 一部の解像度では音声がありません プレーヤーのジェスチャー制御 ジェスチャーを使用してプレーヤーの明るさと音量をコントロールする - 検索候補 + 検索候補の表示 検索時に候補を表示します 最高の解像度 NewPipe について @@ -170,13 +170,13 @@ 登録リスト 新着 検索履歴 - 検索履歴をローカルに保存します + 検索した履歴を記憶します 再生履歴とキャッシュ - 再生した動画を記録します - フォーカスで再開 + 再生した履歴を記憶します + オーディオフォーカス復帰で再開する 電話などによる中断の後、再生を再開します プレーヤー - 動画の詳細ページで背景、またはポップアップボタンが押されたときにヒントを表示する + 動画の詳細ページで、背景またはポップアップボタンが押されたときにヒントを表示する 動作 履歴とキャッシュ プレイリスト @@ -261,10 +261,10 @@ ブックマーク サムネイルを読み込む 画像キャッシュを消去しました - メタデータのキャッシュを消去 - ウェブページのキャッシュデータをすべて削除します - メタデータのキャッシュを消去しました - 次の動画を自動でキューに追加 + キャッシュを消去 + アプリ内のキャッシュをデータすべて削除します + キャッシュが消去されました + 関連動画を自動でキューに追加する デバッグ ファイル 動画が見つかりません @@ -284,7 +284,7 @@ すべてのアイテムを再生履歴から削除しますか? 常に確認 新規プレイリスト - 削除 + 削除する 変更 プレイリスト名 プレイリストに追加 @@ -296,7 +296,7 @@ プレイリストに追加しました プレイリストのサムネイルを変更しました。 プレイリストを削除できませんでした。 - 字幕なし + 字幕表示なし 字幕の文字サイズ インポート/エクスポート インポート @@ -305,7 +305,7 @@ インポートしています… エクスポートしています… ファイルからインポート - 前回のエクスポート + 前回のエクスポート先 購読リストがインポートできませんでした 購読リストがエクスポートできませんでした 速度 @@ -330,7 +330,7 @@ おおまかなシーク おおまかなシークを使用すると、正確さが下がりますが高速なシークが可能になります すべてのサムネイルの読み込みと保存を無効化します、このオプションを切り替えるとメモリおよびディスク上の画像キャッシュがクリアされます。 - 繰り返しでないキューの最後の動画を再生時、関連動画を自動的にキューに追加する。 + 繰り返しではないキューの再生後、関連動画を自動的にキューに追加します すべての再生履歴を削除しますか? すべての検索履歴を削除しますか? このファイル/コンテンツはありません @@ -352,13 +352,13 @@ 最も再生した動画 ズーム プレイリスト - 「長押しして追加」のヒントを表示 + 「長押しして追加」のヒントを表示する トラック NewPipe バックグラウンドおよびポップアップのプレーヤーの通知 新着 & 人気 長押ししてキューに入れる - バックグラウンド時にキューに入れる - ポップアップ時にキューに入れる + バックグラウンド再生のキューに入れる + ポップアップ再生のキューに入れる ポップアップ時にここから開始 すぐにここに表示されます;D お好みの \'開く\' アクション @@ -366,7 +366,7 @@ フィット 全画面 自動生成 - 字幕の文字サイズと背景色を変更します。有効にするにはアプリの再起動が必要です。 + アプリの再起動後、設定した字幕設定が反映されます 何もありません 保存したエクスポートファイルからYouTubeの購読をインポート: \n @@ -398,13 +398,13 @@ \n \n続行しますか\? 再生速度を変更 - 連動しない (歪むかもしれません) + 速度と音程を連動せずに変更 (歪むかもしれません) 無音の間に早送り 音程幅 購読解除 新しいタブ タブを選択 - 更新 + アプリの更新 催し物 新しい NewPipe バージョンの通知 外部記憶装置は利用できません @@ -415,7 +415,7 @@ 会議 ヨーロッパの一般データ保護規制(GDPR)に準拠するために、NewPipeの個人情報保護方針にご注意ください。よく読んでください。 \n私たちに不具合報告を送るためには、これを受け入れなければなりません。 - 更新 + アプリの更新 リストビュー モード リスト グリッド @@ -428,7 +428,7 @@ 一時停止 順番待ちに追加しました 保存処理をしています - 順に処理する + 順番に処理する 操作がシステムによって拒否されました ダウンロードに失敗しました ダウンロードが完了しました @@ -450,11 +450,11 @@ 必要な範囲が満たされていません 見つかりません 保存処理に失敗しました - 完了済みのダウンロードを削除 + 完了済みを一覧から削除します 停止 最大再試行回数 ダウンロードを中止するまでの最大再試行回数 - 音量ジェスチャー制御 + 音量のジェスチャー制御 ジェスチャーを使用して、プレーヤーの音量を制御します 明るさのジェスチャー制御 ジェスチャーを使用して、プレーヤーの明るさを制御します @@ -466,5 +466,5 @@ 新しいバージョンが利用可能なときにアプリの更新を確認する通知を表示します ダウンロードから %s の保留中の転送を続行します モバイルデータ通信に切替時に、一時停止する - 一時停止できないダウンロードは再開されます + 一時停止できない場合は再開して継続されます \ No newline at end of file From d8e6ad48ca16d916964267d0762b5d5d2c05f465 Mon Sep 17 00:00:00 2001 From: ssantos Date: Sat, 1 Jun 2019 10:19:15 +0000 Subject: [PATCH 116/138] Translated using Weblate (Portuguese) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-pt/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index de0097bb3..9db2e401a 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -344,7 +344,7 @@ Controlos para velocidade de reprodução Ritmo Limpar histórico de visualizações - Adicionar automaticamente à fila uma emissão relacionada se em filas não repetíveis. + Auto-aplicar uma emissão relacionada ao reproduzir a último emissão numa fila não repetitiva Mostrar dica \"Toque longo para colocar na fila\" Mostrar dica quando o botão de fundo ou de \'popup\' for premido na página de detalhes do vídeo Canais From 407c61e212bb6007192d50837c4155627f867695 Mon Sep 17 00:00:00 2001 From: bob mar Date: Sun, 2 Jun 2019 10:25:50 +0000 Subject: [PATCH 117/138] Translated using Weblate (Hebrew) Currently translated at 99.8% (442 of 443 strings) --- app/src/main/res/values-he/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 155c6a223..9ddc13d79 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -98,7 +98,7 @@ דיווח מידע: מה קרה: - מה:\\nבקשה:\\nשפת התוכן:\\nשירות:\\nשעון גריניץ׳:\\nחבילה:\\nגרסה:\\nגרסת מערכת ההפעלה: + מה:\\nבקשה:\\nשפת התוכן:\\nשירות:\\nזמן גריניץ\'GMT:\\nחבילה:\\nגרסה:\\nגרסת מערכת ההפעלה: עריכת מינוי נרשמת ביטול מינוי לערוץ @@ -272,7 +272,7 @@ נגנים חיצוניים לא תומכים בסוגי קישורים כאלה כתובת שגויה קובץ - העברה לרקע + החלף לרקע העברה לחלון צף לא נמצא נגן צפייה ישירה (ניתן להתקין את VLC כדי לתקן זאת). תמונות מטמון נמחקו From fb18ea7ff88d3cc167e6a4d58d16c69704ed0f95 Mon Sep 17 00:00:00 2001 From: artik banana Date: Sun, 2 Jun 2019 10:26:11 +0000 Subject: [PATCH 118/138] Translated using Weblate (Hebrew) Currently translated at 99.8% (442 of 443 strings) --- app/src/main/res/values-he/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 9ddc13d79..18b12e868 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -267,8 +267,8 @@ ניפוי שגיאות תמיד חד פעמי - ייבוא מסד נתונים - ייצוא מסד נתונים + ייבוא מאגר מידע + ייצוא מאגר מידע נגנים חיצוניים לא תומכים בסוגי קישורים כאלה כתובת שגויה קובץ @@ -285,7 +285,7 @@ היסטוריית החיפוש נמחקה. הקובץ אינו קיים או שחסרה הרשאה לקרוא אותו או לכתוב אליו שם הקובץ אינו יכול להיות ריק - ניתן לסדר מחדש בגרירה + גרור לשינוי הסדר יצירה למחוק אחד למחוק הכול From aefc51db4b56a2b6f9fa0b6ae1b454aaac1a104b Mon Sep 17 00:00:00 2001 From: AB Date: Sun, 2 Jun 2019 06:51:59 +0000 Subject: [PATCH 119/138] Translated using Weblate (Ukrainian) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-uk/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index ffc996f49..272ebc3b2 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -325,7 +325,7 @@ Використовувати швидкий неточний пошук Неточний пошук дозволяє програвачеві рухатися позиціями швидше, проте з меншою точністю Автоматично додавати в чергу наступний запис - Автоматично додавати пов\'язаний запис під час відтворення останнього у черзі без повторювань. + Автоматично додавати пов\'язаний запис під час відтворення останнього у черзі без повторювань Файл Такої теки не існує Такого джерела файлу/контенту не існує From d8cb950248951498c7014a54e581ffffd1ed9e97 Mon Sep 17 00:00:00 2001 From: Joseph Kim Date: Sun, 2 Jun 2019 01:41:12 +0000 Subject: [PATCH 120/138] Translated using Weblate (Korean) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-ko/strings.xml | 174 ++++++++++++------------- 1 file changed, 81 insertions(+), 93 deletions(-) diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index bcaafbcfc..068312727 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -30,7 +30,6 @@ 지원하지 않는 URL 입니다 기본 컨텐츠 언어 비디오 & 오디오 - 비디오 미리보기 썸네일 비디오 미리보기 썸네일 업로더 썸네일 @@ -38,24 +37,19 @@ 좋아요 외부 비디오 플레이어 사용 외부 오디오 플레이어 사용 - 오디오 다운로드 폴더 다운로드된 오디오는 여기에 저장됩니다 오디오 파일 다운로드 경로를 입력하세요 - 테마 어두운 테마 밝은 테마 - 외관 기타 백그라운드에서 재생 중 재생 네트워크 오류 - Tor 사용 (실험적) 향상된 프라이버시를 위해 다운로드 트래픽을 강제로 Tor를 통해 전송 (스트리밍 비디오는 아직 지원되지 않습니다). - 다운로드 디렉토리를 만들 수 없습니다 \'%1$s\' 다운로드 디렉토리를 만들었습니다 \'%1$s\' 검색 버튼을 눌러서 시작하세요 @@ -65,7 +59,6 @@ 연령 제한 컨텐츠 연령 제한 비디오입니다. 설정 메뉴에서 시청 허용 여부를 변경하실 수 있습니다. 라이브 - 오류 모든 썸네일을 불러올 수 없습니다 비디오 URL 서명을 복호화할 수 없습니다 @@ -84,11 +77,8 @@ 다음이 발생함: 내용 (영어로 작성): 자세한 사항: - - 오류 보고 사용자 보고서 - 비디오 오디오 재시도 @@ -99,8 +89,7 @@ 일시정지 삭제 체크섬 - -팝업 모드에서 열기 + 팝업 모드에서 열기 일부 해상도에서 소리가 나지 않습니다 뉴파이프 팝업 모드 구독 @@ -108,15 +97,11 @@ 채널 구독 해제됨 구독 여부를 변경할 수 없음 구독을 업데이트할 수 없음 - 메인 화면 구독 - 새로운 영상 - 배경 팝업 - 기본 팝업 해상도 높은 해상도 표시 일부 기기에서만 2K/4K 해상도 재생이 지원됩니다 @@ -157,48 +142,37 @@ 최대 해상도 되돌리기 전부 재생 - 뉴파이프 알림 뉴파이프 백그라운드 및 팝업 플레이어 알림 - [알 수 없음] - 이미지를 불러올 수 없습니다 앱/UI 충돌 이 스트림을 재생할 수 없습니다 복구할 수 없는 플레이어 오류가 발생했습니다 플레이어 오류로부터 복구 중 - 무엇을:\\n요청:\\n컨텐츠 언어:\\n서비스:\\nGMT 기준 시간:\\n패키지:\\n버전:\\n안드로이드 버전: 결과 없음 구독할 항목을 추가하세요 - 구형 플레이어 사용 내장된 구형 Mediaframework 플레이어 사용 - 백만 10억 - 구독자 없음 - %s 구독자 - - + %s 구독자 + 시청 횟수 없음 - %s 시청 횟수 - - + %s 시청 횟수 + 비디오 없음 - 비디오 - - + 비디오 + 재생 새로운 미션 OK - 파일명 쓰레드 오류 @@ -212,18 +186,14 @@ 다운로드 할 폴더를 선택하세요 이 권한은 팝업 모드에서 \n열기 위해 필요합니다 - 로봇인지 확인 (reCAPTCHA) reCAPTCHA challenge 요청됨 - 다운로드 파일명에 허용되는 문자 올바르지 않은 문자는 다음 문자로 대체됩니다 대체 문자 - 문자 및 숫자 가장 특수한 문자 - 뉴파이프에 대해서 설정 뉴파이프 @@ -245,8 +215,6 @@ 뉴파이프에 관한 최신 및 상세 정보를 얻으려면 웹사이트를 방문하세요. 뉴파이프가 채택한 라이센스 라이센스 읽기 - - 기록 검색함 시청함 @@ -256,7 +224,6 @@ 기록이 삭제되었습니다 항목이 삭제되었습니다 이 항목을 검색 기록에서 삭제할까요? - 메인 화면의 내용 빈 페이지 키오스크 페이지 @@ -266,7 +233,6 @@ 채널 선택 구독중인 채널이 없습니다 키오스크 선택 - 키오스크 인기 급상승 탑 50 @@ -276,20 +242,17 @@ 제거 상세 정보 오디오 설정 - 눌러서 대기 목록에 추가 - 백그라운드에 대기 - 팝업에 대기 + 눌러서 대기열에 추가 + 백그라운드로 갈 경우 대기 + 새 팝업으로 갈 경우 대기 여기서부터 재생 - 여기서부터 백그라운드에서 재생 - 여기서부터 팝업에 재생 -스트리밍 플레이어를 찾을 수 없습니다. VLC를 설치하면 플레이하실 수 있습니다 + 백그라운드로 갈 경우 여기서부터 재생 + 새 팝업으로 갈 경우 여기서부터 재생 + 스트리밍 플레이어를 찾을 수 없습니다. VLC를 설치하면 플레이하실 수 있습니다. 스트리밍 파일 다운로드하기 정보 보기 - 플레이리스트 북마크 - 이곳에 추가 - 정확하지는 않지만 빠른 탐색 정확하지 않은 탐색은 빠르게 위치로 탐색할 수 있지만 정확도는 떨어집니다 다음 스트림을 자동으로 재생열에 추가하기 @@ -300,12 +263,10 @@ 라이브 (LIVE) 항상 한번만 - 디바이스 방향 토글 백그라운드로 전환 팝업으로 전환 기본으로 전환 - 데이터베이스 가져오기 데이터베이스 내보내기 현재 시청 기록 및 구독 목록을 덮어쓰기 합니다 @@ -314,102 +275,79 @@ 잘못된 URL 발견된 비디오 스트림 없음 발견된 오디오 스트림 없음 - 드래그하여 재배열 - 만들기 1개 삭제하기 모두 삭제하기 취소 이름 바꾸기 - 로봇인지 확인합니다 이 항목을 시청 기록에서 삭제하시겠습니까? 모든 항목을 시청 기록에서 삭제하시겠습니까? 마지막으로 재생 가장 많이 재생 - 내보내기 완료 가져오기 완료 유효한 ZIP 파일 없음 경고: 모든 파일 가져오기를 실패했습니다. 이것은 현재 설정을 덮어쓸 것입니다. - 드로어 열기 드로어 닫기 여기에 무언가가 추가될 거에요~ :D - - 비디오 플레이어 백그라운드 플레이어 팝업 플레이어 항상 묻기 - 정보 가져오는 중… 요청한 콘텐츠를 로딩 중입니다 - 새로운 재생목록 삭제 이름 바꾸기 이름 재생목록에 추가 재생목록 썸네일로 설정 - 재생목록 북마크하기 북마크 제거하기 - 이 재생목록을 삭제하시겠습니까? 재생목록 생성 완료 재생목록에 추가됨 재생목록 썸내일이 바뀜. 재생목록을 삭제할 수 없습니다. - 자막 없음 - 꼭 맞게 하기 채우기 확대 - 자동 생성됨 자막 폰트 크기 작은 폰트 보통 폰트 큰 폰트 - 동기화 - LeakCanary 할성화 힙 덤프 중 메모리 누수 점검으로 앱이 불안정해질 수 있습니다 - out-of-lifecycle 오류 보고 프래그먼트 또는 버려진 액티비티 주기 밖에서 일어나는 전달할 수 없는 Rx 예외를 강제적으로 보고하기 - -파일 - + 파일 폴더가 존재하지 않습니다 - 잘못된 파일/콘덴츠 소스 + 잘못된 파일/콘텐츠 소스 파일이 존재하지 않거나 읽기/쓰기 권한이 없습니다 파일명이 비어 있으면 안됩니다 오류 발생: %1$s - 가져오기/내보내기 가져오기 이곳으로부터 가져오기 이곳으로 내보내기 - 가져오는 중.… 내보내는 중… - 파일 가져오기 이전 내보내기 - 구독 목록 가져오기 실패 구독 목록 내보내기 실패 - - YouTube 구독 목록을 가져오려면 내보내기 파일이 필요합니다. 다운로드 하려면 -\n1. 이곳으로 가세요: $1$s -\n2. 로그인이 필요하면 하세요 -\n3. 다운로드가 곧 시작 됩니다 (이 파일이 내보내기 파일 입니다) + \'YouTube 구독 파일\'을 다운로드해서 구독 목록을 가져올 수 있습니다: +\n +\n1. 이곳으로 가세요: $1$s +\n2. 요청에 따라 로그인을 진행합니다 +\n3. 다운로드가 곧 시작 됩니다 (이 파일이 구독 파일입니다) SoundCloud 프로필을 가져오시려면 URL 및 ID를 입력해주세요. \n \n프로필 URL을 찾으시려면 다음 과정을 따라해 주세요. @@ -418,11 +356,10 @@ \n3. 로그인이 필요하면 하세요. \n4. 리디렉트된 곳의 URL을 복사하세요. (이 URL이 당신의 프로필 URL 입니다) 프로필ID, soundcloud.com/프로필ID - 경고: 데이터 소모량이 늘어날 수 있습니다. \n \n계속하시겠습니까? -썸내일 로드하기 + 썸내일 로드하기 동영상 썸네일을 로드하지 않으며, 데이터와 메모리 사용을 최대한 줄입니다. 이 옵션을 \n선택 시 모든 메모리 캐시와 저장소 캐시를 삭제합니다. 이미지 캐시 지워짐 @@ -435,16 +372,12 @@ 영상과 소리 분리 (소리가 깨질 수 있음) 나이트코어 기본 -다운로드 가능한 스트림이 없습니다 - + 다운로드 가능한 스트림이 없습니다 이 파일을 재생할 수 있는 플레이어 앱이 없습니다 - 선호하는 열기 동작 컨텐츠를 열 때 사용할 기본 동작 — %s - 자막 플레이어 자막 텍스트 크기와 배경 스타일을 변경합니다. 효과를 적용하려면 앱을 재시작 해야합니다. - 채널만 재생 목록만 시청 기록 삭제하기 @@ -459,15 +392,12 @@ 뉴파이프 프로젝트는 사용자의 개인 정보 보호를 최우선으로 생각하며, 동의 없이 어떠한 정보도 수집하지 않습니다. \n뉴파이프 개인정보 보호 정책에서는 오류 보고 시 어떠한 정보가 수집되고 저장되는지 자세히 명시되어 있습니다. 개인정보 보호 정책 읽기 - 뉴파이프는 카피레프트 자유 소프트웨어입니다. 사용자는 이 앱을 사용, 공유, 또는 수정하는 것이 가능하고, 수정 후 재배포 시 자유 소프트웨어 재단의 GNU 라이센스 버전 -\n3 또는 그 이상의 버전을 포함해야 합니다. + 뉴파이프는 카피레프트 자유 소프트웨어입니다. 사용자는 이 앱을 사용, 공유, 또는 수정할 수 있고, 수정 후 재배포 시 자유 소프트웨어 재단의 GNU 라이센스 버전 3 또는 그 이상의 버전을 포함해야 합니다. 앱 설정을 가져오시겠습니까? - 무음 구간 스킵 유럽 연합 일반 데이터 보호 규정 (GDPR) 에 따라, 사용자는 뉴파이프 개인정보 보호 정책을 읽고 꼼꼼히 확인해야 합니다. 버그 리포트를 보내시려면 개인정보 보호 정책에 동의해주세요. 동의 동의하지 않음 - 데이터 제한 없음 모바일 데이터 사용 시 화질 제한 구독 해제 @@ -486,4 +416,62 @@ 새 뉴파이프 버전을 알림 외부 저장소 없음 다운로드할 SD 카드를 찾을 수 없습니다. 다운로드 폴더 경로를 초기화 하시겠습니까\? + 1개의 항목이 삭제되었습니다. + 앱 전환시 최소화 + 비디오 플레이어에서 다른 앱으로 전환 시 다음과 같은 동작 실행 — %s + 없음 + 백그라운드 플레이어로 최소화 + 팝업 플레이어로 최소화 + 단계 + 초기화 + 저장된 탭을 읽는 중 오류가 발생하여 기본 탭을 사용합니다 + 기본값 복원 + 기본값을 복원할까요\? + 구독자 숫자가 없습니다 + 메인 화면에 표시할 탭 + 선택 + 업데이트 + 새 버전이 있을 경우 앱을 업데이트하도록 알림 표시 + \'목록으로 보기\' 모드 + 목록 + 격자 + 자동 + 보기 방식 전환 + NewPipe 업데이트가 있습니다! + 여기를 눌러서 다운로드 + 완료됨 + 대기열에 있음 + 일시중지됨 + 대기열에 추가됨 + 후처리 실행 중 + 대기열 + 시스템에 의해 실행이 거부되었습니다 + 다운로드 실패 + 다운로드 완료 + %s 다운로드 완료됨 + 별개의 이름 생성 + 덮어쓰기 + 해당 이름을 가진 다운로드된 파일이 이미 존재합니다 + 해당 이름을 가진 다운로드가 이미 진행중입니다 + 오류 표시 + 코드 + 파일을 만들 수 없습니다 + 지정한 폴더를 만들 수 없습니다 + 시스템에 의해 권한이 거부되었습니다 + 보안 연결 실패 + 서버를 찾을 수 없습니다 + 서버에 접속할 수 없습니다 + 서버가 데이터를 전송하지 않고 있습니다 + 서버가 다중 스레드 다운로드를 받아들이지 않습니다, @string/msg_threads = 1 를 사용해 다시 시도해보세요 + 요청된 HTTP 범위가 충분하지 않습니다 + HTTP 찾을 수 없습니다 + 후처리 작업이 실패하였습니다 + 완료된 다운로드 비우기 + 대기중인 %s 다운로드를 지속하세요 + 멈추기 + 최대 재시도 횟수 + 다운로드를 취소하기 전까지 다시 시도할 최대 횟수 + 모바일 데이터로 전환시 일시정지 + 일시정지 할 수 없는 다운로드의 경우에는 다시 시작됩니다 + 컨퍼런스 \ No newline at end of file From a9f3939c8303da44924d537c9ab46f9d13008774 Mon Sep 17 00:00:00 2001 From: artik banana Date: Sun, 2 Jun 2019 10:27:07 +0000 Subject: [PATCH 121/138] Translated using Weblate (Hebrew) Currently translated at 99.8% (442 of 443 strings) --- app/src/main/res/values-he/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 18b12e868..957448045 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -364,7 +364,7 @@ \nמדיניות הפרטיות של NewPipe מסבירה בפרטי פרטים אילו נתונים נשלחים ומאוחסנים בעת שליחת דיווח על תקלה. הצגת מדיניות הפרטיות NewPipe הוא יישומון חופשי בהתאם לרישיון קופילפט: מותר לך להשתמש, לחקור, לשתף ולשפר בכל דרך שנראית לך. במיוחד מותר לך להפיץ מחדש ו/או לשנות תחת תנאי הרישיון הציבורי הכללי של GNU כפי שמופץ על ידי קרן התכנה החופשית, בין אם גרסה 3 של הרישיון או (לשיקולך) כל גרסה עדכנית יותר שלו. - נוגן אחרון + התנגן לאחרונה הכי נצפים אזהרה: ייבוא חלק מהקבצים נכשל. פעולה זו תדרוס את ההגדרות הקיימות. From 4a8baaef45776712846b94fa4ce088d38fb74086 Mon Sep 17 00:00:00 2001 From: bob mar Date: Sun, 2 Jun 2019 10:27:22 +0000 Subject: [PATCH 122/138] Translated using Weblate (Hebrew) Currently translated at 99.8% (442 of 443 strings) --- app/src/main/res/values-he/strings.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 957448045..2e8626c7f 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -297,7 +297,7 @@ למחוק את כל הפריטים מההיסטוריה\? הייצוא הסתיים הייבוא הסתיים - אין קובץ ZIP תקין + קובץ ZIP לא תקין נגן סרטונים נגן רקע נגן צף @@ -309,7 +309,7 @@ שינוי שם שם הוספה לרשימת נגינה - הוספה רשימת השמעה לסימניות + קשר רשימת השמעה למועדפים הסרת סימנייה למחוק רשימת נגינה זו\? רשימת הנגינה נוצרה @@ -319,9 +319,9 @@ תקריב נוצרו אוטומטית ייבוא/ייצוא - ייבוא - ייבוא מ־ - ייצוא אל + יבא + יבא מ + יצא ל מתבצע ייבוא… מתבצע ייצוא… ייבוא קובץ From f8ccc3128e1fcc4b110eec0daf64868d2309b418 Mon Sep 17 00:00:00 2001 From: Yaron Shahrabani Date: Sun, 2 Jun 2019 10:42:11 +0000 Subject: [PATCH 123/138] Translated using Weblate (Hebrew) Currently translated at 99.8% (442 of 443 strings) --- app/src/main/res/values-he/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 2e8626c7f..83deb2b4f 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -98,7 +98,7 @@ דיווח מידע: מה קרה: - מה:\\nבקשה:\\nשפת התוכן:\\nשירות:\\nזמן גריניץ\'GMT:\\nחבילה:\\nגרסה:\\nגרסת מערכת ההפעלה: + מה:\\nבקשה:\\nשפת התוכן:\\nשירות:\\nשעון גריניץ׳:\\nחבילה:\\nגרסה:\\nגרסת מערכת ההפעלה: עריכת מינוי נרשמת ביטול מינוי לערוץ @@ -267,8 +267,8 @@ ניפוי שגיאות תמיד חד פעמי - ייבוא מאגר מידע - ייצוא מאגר מידע + ייבוא מסד נתונים + ייצוא מסד נתונים נגנים חיצוניים לא תומכים בסוגי קישורים כאלה כתובת שגויה קובץ From c7cb6523225850cfd875ae727eba57de78a9fcc6 Mon Sep 17 00:00:00 2001 From: Yaron Shahrabani Date: Sun, 2 Jun 2019 10:44:08 +0000 Subject: [PATCH 124/138] Translated using Weblate (Hebrew) Currently translated at 99.8% (442 of 443 strings) --- app/src/main/res/values-he/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 83deb2b4f..80d8daf51 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -320,7 +320,7 @@ נוצרו אוטומטית ייבוא/ייצוא יבא - יבא מ + ייבוא מ־ יצא ל מתבצע ייבוא… מתבצע ייצוא… From ce204eba62e06861688d700abff1e158ac427407 Mon Sep 17 00:00:00 2001 From: Yaron Shahrabani Date: Sun, 2 Jun 2019 10:44:25 +0000 Subject: [PATCH 125/138] Translated using Weblate (Hebrew) Currently translated at 100.0% (443 of 443 strings) --- app/src/main/res/values-he/strings.xml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 80d8daf51..feeb5098b 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -272,7 +272,7 @@ נגנים חיצוניים לא תומכים בסוגי קישורים כאלה כתובת שגויה קובץ - החלף לרקע + העברה לרקע העברה לחלון צף לא נמצא נגן צפייה ישירה (ניתן להתקין את VLC כדי לתקן זאת). תמונות מטמון נמחקו @@ -285,7 +285,7 @@ היסטוריית החיפוש נמחקה. הקובץ אינו קיים או שחסרה הרשאה לקרוא אותו או לכתוב אליו שם הקובץ אינו יכול להיות ריק - גרור לשינוי הסדר + ניתן לסדר מחדש בגרירה יצירה למחוק אחד למחוק הכול @@ -297,7 +297,7 @@ למחוק את כל הפריטים מההיסטוריה\? הייצוא הסתיים הייבוא הסתיים - קובץ ZIP לא תקין + אין קובץ ZIP תקין נגן סרטונים נגן רקע נגן צף @@ -309,7 +309,7 @@ שינוי שם שם הוספה לרשימת נגינה - קשר רשימת השמעה למועדפים + הוספת רשימת נגינה לסימניות הסרת סימנייה למחוק רשימת נגינה זו\? רשימת הנגינה נוצרה @@ -319,9 +319,9 @@ תקריב נוצרו אוטומטית ייבוא/ייצוא - יבא + ייבוא ייבוא מ־ - יצא ל + ייצוא אל מתבצע ייבוא… מתבצע ייצוא… ייבוא קובץ @@ -341,7 +341,7 @@ כיבוי האפשרות מונע את טעינת התמונות הממוזערות, חוסך בתקשורת נתונים ובניצולת הזיכרון. שינויים באפשרות זו מוחקים את המטמון בזיכרון ובכונן. הסרת כל נתוני העמודים שבמטמון הוספת התזרים הבא לרשימת הנגינה אוטומטית - להוסיף אוטומטית תזרים דומה בעת נגינת התזרים האחרון בתור שאינו מחזורי. + להוסיף אוטומטית תזרים דומה בעת נגינת התזרים האחרון בתור שאינו מחזורי החלפת כיווניות העברה לראשי משכתב את ההיסטוריה והמינויים הנוכחיים שלך @@ -364,7 +364,7 @@ \nמדיניות הפרטיות של NewPipe מסבירה בפרטי פרטים אילו נתונים נשלחים ומאוחסנים בעת שליחת דיווח על תקלה. הצגת מדיניות הפרטיות NewPipe הוא יישומון חופשי בהתאם לרישיון קופילפט: מותר לך להשתמש, לחקור, לשתף ולשפר בכל דרך שנראית לך. במיוחד מותר לך להפיץ מחדש ו/או לשנות תחת תנאי הרישיון הציבורי הכללי של GNU כפי שמופץ על ידי קרן התכנה החופשית, בין אם גרסה 3 של הרישיון או (לשיקולך) כל גרסה עדכנית יותר שלו. - התנגן לאחרונה + התנגנו אחרונים הכי נצפים אזהרה: ייבוא חלק מהקבצים נכשל. פעולה זו תדרוס את ההגדרות הקיימות. From 1684a2110cfc94ca44bb2c3bbe789a0b9e4d3879 Mon Sep 17 00:00:00 2001 From: Tobias Groza Date: Mon, 3 Jun 2019 22:06:58 +0200 Subject: [PATCH 126/138] Update Extractor --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index da735302c..820e02570 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -57,7 +57,7 @@ dependencies { exclude module: 'support-annotations' }) - implementation 'com.github.TeamNewPipe:NewPipeExtractor:2ac713e' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:c64c90a' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:2.23.0' From 9e34fee58c1bd2ba9040a5457f73b9e7cc36a256 Mon Sep 17 00:00:00 2001 From: kapodamy Date: Fri, 22 Mar 2019 22:54:07 -0300 Subject: [PATCH 127/138] New MP4 muxer + Queue changes + Storage fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Main changes: * correctly check the available space (CircularFile.java) * misc cleanup (CircularFile.java) * use the "Error Reporter" for non-http errors * rewrite network state checking and add better support for API 21 (Lollipop) or higher * implement "metered networks" * add buttons in "Downloads" activity to start/pause all pending downloads, ignoring the queue flag or if the network is "metered" * add workaround for VPN connections and/or network switching. Example: switching WiFi to 3G * rewrite DataReader ¡Webm muxer is now 57% more faster! * rewrite CircularFile, use file buffers instead of memory buffers. Less troubles in low-end devices * fix missing offset for KaxCluster (WebMWriter.java), manifested as no thumbnails on file explorers Download queue: * remember queue status, unless the user pause the download (un-queue) * semi-automatic downloads, between networks. Effective if the user create a new download or the downloads activity is starts * allow enqueue failed downloads * new option, queue limit, enabled by default. Used to allow one or multiple downloads at same time Miscellaneous: * fix crash while selecting details/error menu (mistake on MissionFragment.java) * misc serialize changes (DownloadMission.java) * minor UI tweaks * allow overwrite paused downloads * fix wrong icons for grid/list button in downloads * add share option * implement #2006 * correct misspelled word in strings.xml (es) (cmn) * fix MissionAdapter crash during device shutdown New Mp4Muxer + required changes: * new mp4 muxer (from dash only) with this, muxing on Android 7 is possible now!!! * re-work in SharpStream * drop mp4 dash muxer * misc changes: add warning in SecondaryStreamHelper.java, * strip m4a DASH files to normal m4a format (youtube only) Fix storage issues: * warn to the user if is choosing a "read only" download directory (for external SD Cards), useless is rooted :) * "write proof" allow post-processing resuming only if the device ran out of space * implement "insufficient storage" error for downloads --- .../newpipe/download/DownloadDialog.java | 61 +- .../org/schabi/newpipe/report/UserAction.java | 4 +- .../settings/DownloadSettingsFragment.java | 14 +- .../schabi/newpipe/streams/DataReader.java | 237 ++++- .../schabi/newpipe/streams/Mp4DashReader.java | 444 +++++++--- .../schabi/newpipe/streams/Mp4DashWriter.java | 623 -------------- .../newpipe/streams/Mp4FromDashWriter.java | 810 ++++++++++++++++++ .../newpipe/streams/SubtitleConverter.java | 15 +- .../newpipe/streams/TrackDataChunk.java | 65 -- .../schabi/newpipe/streams/WebMReader.java | 16 +- .../schabi/newpipe/streams/WebMWriter.java | 130 ++- .../newpipe/streams/io/SharpStream.java | 22 +- .../org/schabi/newpipe/util/ListHelper.java | 14 +- .../newpipe/util/SecondaryStreamHelper.java | 2 +- .../giga/get/DownloadInitializer.java | 3 - .../us/shandian/giga/get/DownloadMission.java | 122 ++- .../java/us/shandian/giga/get/Mission.java | 5 + .../giga/postprocessing/M4aNoDash.java | 43 + ...p4DashMuxer.java => Mp4FromDashMuxer.java} | 58 +- .../giga/postprocessing/Mp4Muxer.java | 136 --- .../giga/postprocessing/Postprocessing.java | 118 ++- .../io/ChunkFileInputStream.java | 5 +- .../giga/postprocessing/io/CircularFile.java | 375 -------- .../postprocessing/io/CircularFileWriter.java | 459 ++++++++++ .../giga/postprocessing/io/FileStream.java | 126 --- .../postprocessing/io/SharpInputStream.java | 4 +- .../giga/service/DownloadManager.java | 227 +++-- .../giga/service/DownloadManagerService.java | 143 ++-- .../giga/ui/adapter/MissionAdapter.java | 255 ++++-- .../giga/ui/fragment/MissionsFragment.java | 33 +- .../res/drawable-hdpi/ic_pause_black_24dp.png | Bin 0 -> 135 bytes .../res/drawable-hdpi/ic_pause_white_24dp.png | Bin 0 -> 138 bytes .../res/drawable-mdpi/ic_pause_black_24dp.png | Bin 0 -> 109 bytes .../res/drawable-mdpi/ic_pause_white_24dp.png | Bin 0 -> 112 bytes .../drawable-xhdpi/ic_pause_black_24dp.png | Bin 0 -> 162 bytes .../drawable-xhdpi/ic_pause_white_24dp.png | Bin 0 -> 139 bytes .../drawable-xxhdpi/ic_pause_black_24dp.png | Bin 0 -> 196 bytes .../drawable-xxhdpi/ic_pause_white_24dp.png | Bin 0 -> 206 bytes .../drawable-xxxhdpi/ic_pause_black_24dp.png | Bin 0 -> 248 bytes .../drawable-xxxhdpi/ic_pause_white_24dp.png | Bin 0 -> 254 bytes app/src/main/res/menu/download_menu.xml | 12 + app/src/main/res/menu/mission.xml | 13 +- app/src/main/res/values-cmn/strings.xml | 2 +- app/src/main/res/values-es/strings.xml | 30 +- app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/settings_keys.xml | 1 + app/src/main/res/values/strings.xml | 14 +- app/src/main/res/values/styles.xml | 2 + app/src/main/res/xml/download_settings.xml | 7 + 49 files changed, 2715 insertions(+), 1936 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/streams/Mp4DashWriter.java create mode 100644 app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java delete mode 100644 app/src/main/java/org/schabi/newpipe/streams/TrackDataChunk.java create mode 100644 app/src/main/java/us/shandian/giga/postprocessing/M4aNoDash.java rename app/src/main/java/us/shandian/giga/postprocessing/{Mp4DashMuxer.java => Mp4FromDashMuxer.java} (60%) delete mode 100644 app/src/main/java/us/shandian/giga/postprocessing/Mp4Muxer.java delete mode 100644 app/src/main/java/us/shandian/giga/postprocessing/io/CircularFile.java create mode 100644 app/src/main/java/us/shandian/giga/postprocessing/io/CircularFileWriter.java delete mode 100644 app/src/main/java/us/shandian/giga/postprocessing/io/FileStream.java create mode 100644 app/src/main/res/drawable-hdpi/ic_pause_black_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_pause_white_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_pause_black_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_pause_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_pause_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_pause_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_pause_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_pause_white_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_pause_black_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_pause_white_24dp.png diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index ec6d42b29..0b4767133 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -7,6 +7,7 @@ import android.preference.PreferenceManager; import android.support.annotation.IdRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.StringRes; import android.support.v4.app.DialogFragment; import android.support.v7.app.AlertDialog; import android.support.v7.widget.Toolbar; @@ -52,6 +53,7 @@ import icepick.State; import io.reactivex.disposables.CompositeDisposable; import us.shandian.giga.postprocessing.Postprocessing; import us.shandian.giga.service.DownloadManagerService; +import us.shandian.giga.service.DownloadManagerService.MissionCheck; public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener { private static final String TAG = "DialogFragment"; @@ -263,7 +265,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck } @Override - public void onSaveInstanceState(Bundle outState) { + public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); Icepick.saveInstanceState(this, outState); } @@ -476,23 +478,40 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck final String finalFileName = fileName; - DownloadManagerService.checkForRunningMission(context, location, fileName, (listed, finished) -> { - if (listed) { - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(R.string.download_dialog_title) - .setMessage(finished ? R.string.overwrite_warning : R.string.download_already_running) - .setPositiveButton( - finished ? R.string.overwrite : R.string.generate_unique_name, - (dialog, which) -> downloadSelected(context, stream, location, finalFileName, kind, threads) - ) - .setNegativeButton(android.R.string.cancel, (dialog, which) -> { - dialog.cancel(); - }) - .create() - .show(); - } else { - downloadSelected(context, stream, location, finalFileName, kind, threads); + DownloadManagerService.checkForRunningMission(context, location, fileName, (MissionCheck result) -> { + @StringRes int msgBtn; + @StringRes int msgBody; + + switch (result) { + case Finished: + msgBtn = R.string.overwrite; + msgBody = R.string.overwrite_warning; + break; + case Pending: + msgBtn = R.string.overwrite; + msgBody = R.string.download_already_pending; + break; + case PendingRunning: + msgBtn = R.string.generate_unique_name; + msgBody = R.string.download_already_running; + break; + default: + downloadSelected(context, stream, location, finalFileName, kind, threads); + return; } + + // overwrite or unique name actions are done by the download manager + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.download_dialog_title) + .setMessage(msgBody) + .setPositiveButton( + msgBtn, + (dialog, which) -> downloadSelected(context, stream, location, finalFileName, kind, threads) + ) + .setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel()) + .create() + .show(); }); } @@ -503,14 +522,18 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck String secondaryStreamUrl = null; long nearLength = 0; - if (selectedStream instanceof VideoStream) { + if (selectedStream instanceof AudioStream) { + if (selectedStream.getFormat() == MediaFormat.M4A) { + psName = Postprocessing.ALGORITHM_M4A_NO_DASH; + } + } else if (selectedStream instanceof VideoStream) { SecondaryStreamHelper secondaryStream = videoStreamsAdapter .getAllSecondary() .get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream)); if (secondaryStream != null) { secondaryStreamUrl = secondaryStream.getStream().getUrl(); - psName = selectedStream.getFormat() == MediaFormat.MPEG_4 ? Postprocessing.ALGORITHM_MP4_MUXER : Postprocessing.ALGORITHM_WEBM_MUXER; + psName = selectedStream.getFormat() == MediaFormat.MPEG_4 ? Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER : Postprocessing.ALGORITHM_WEBM_MUXER; psArgs = null; long videoSize = wrappedVideoStreams.getSizeInBytes((VideoStream) selectedStream); diff --git a/app/src/main/java/org/schabi/newpipe/report/UserAction.java b/app/src/main/java/org/schabi/newpipe/report/UserAction.java index 2b2369ad3..2cca9305a 100644 --- a/app/src/main/java/org/schabi/newpipe/report/UserAction.java +++ b/app/src/main/java/org/schabi/newpipe/report/UserAction.java @@ -17,7 +17,9 @@ public enum UserAction { REQUESTED_KIOSK("requested kiosk"), REQUESTED_COMMENTS("requested comments"), DELETE_FROM_HISTORY("delete from history"), - PLAY_STREAM("Play stream"); + PLAY_STREAM("Play stream"), + DOWNLOAD_POSTPROCESSING("download post-processing"), + DOWNLOAD_FAILED("download failed"); private final String message; diff --git a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java index 8214d7b4b..82c6853d5 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.settings; import android.app.Activity; +import android.app.AlertDialog; import android.content.Intent; import android.os.Bundle; import android.support.annotation.Nullable; @@ -12,6 +13,8 @@ import com.nononsenseapps.filepicker.Utils; import org.schabi.newpipe.R; import org.schabi.newpipe.util.FilePickerActivityHelper; +import java.io.File; + public class DownloadSettingsFragment extends BasePreferenceFragment { private static final int REQUEST_DOWNLOAD_PATH = 0x1235; private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236; @@ -45,7 +48,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { @Override public boolean onPreferenceTreeClick(Preference preference) { if (DEBUG) { - Log.d(TAG, "onPreferenceTreeClick() called with: preference = [" + preference + "]"); + Log.d(TAG, "onPreferenceTreeClick() called with: preference = [" + preference + "]"); } if (preference.getKey().equals(DOWNLOAD_PATH_PREFERENCE) @@ -78,6 +81,15 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { defaultPreferences.edit().putString(key, path).apply(); updatePreferencesSummary(); + + File target = new File(path); + if (!target.canWrite()) { + AlertDialog.Builder msg = new AlertDialog.Builder(getContext()); + msg.setTitle(R.string.download_to_sdcard_error_title); + msg.setMessage(R.string.download_to_sdcard_error_message); + msg.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> { }); + msg.show(); + } } } } diff --git a/app/src/main/java/org/schabi/newpipe/streams/DataReader.java b/app/src/main/java/org/schabi/newpipe/streams/DataReader.java index d0e946eb7..567fa5229 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/DataReader.java +++ b/app/src/main/java/org/schabi/newpipe/streams/DataReader.java @@ -1,9 +1,10 @@ package org.schabi.newpipe.streams; +import org.schabi.newpipe.streams.io.SharpStream; + import java.io.EOFException; import java.io.IOException; - -import org.schabi.newpipe.streams.io.SharpStream; +import java.io.InputStream; /** * @author kapodamy @@ -15,89 +16,237 @@ public class DataReader { public final static int INTEGER_SIZE = 4; public final static int FLOAT_SIZE = 4; - private long pos; - public final SharpStream stream; - private final boolean rewind; + private long position = 0; + private final SharpStream stream; + + private InputStream view; + private int viewSize; public DataReader(SharpStream stream) { - this.rewind = stream.canRewind(); this.stream = stream; - this.pos = 0L; + this.readOffset = this.readBuffer.length; } public long position() { - return pos; + return position; } - public final int readInt() throws IOException { + public int read() throws IOException { + if (fillBuffer()) { + return -1; + } + + position++; + readCount--; + + return readBuffer[readOffset++] & 0xFF; + } + + public long skipBytes(long amount) throws IOException { + if (readCount < 0) { + return 0; + } else if (readCount == 0) { + amount = stream.skip(amount); + } else { + if (readCount > amount) { + readCount -= (int) amount; + readOffset += (int) amount; + } else { + amount = readCount + stream.skip(amount - readCount); + readCount = 0; + readOffset = readBuffer.length; + } + } + + position += amount; + return amount; + } + + public int readInt() throws IOException { primitiveRead(INTEGER_SIZE); return primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3]; } - public final int read() throws IOException { - int value = stream.read(); - if (value == -1) { - throw new EOFException(); - } - - pos++; - return value; + public short readShort() throws IOException { + primitiveRead(SHORT_SIZE); + return (short) (primitive[0] << 8 | primitive[1]); } - public final long skipBytes(long amount) throws IOException { - amount = stream.skip(amount); - pos += amount; - return amount; - } - - public final long readLong() throws IOException { + public long readLong() throws IOException { primitiveRead(LONG_SIZE); long high = primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3]; long low = primitive[4] << 24 | primitive[5] << 16 | primitive[6] << 8 | primitive[7]; return high << 32 | low; } - public final short readShort() throws IOException { - primitiveRead(SHORT_SIZE); - return (short) (primitive[0] << 8 | primitive[1]); - } - - public final int read(byte[] buffer) throws IOException { + public int read(byte[] buffer) throws IOException { return read(buffer, 0, buffer.length); } - public final int read(byte[] buffer, int offset, int count) throws IOException { - int res = stream.read(buffer, offset, count); - pos += res; + public int read(byte[] buffer, int offset, int count) throws IOException { + if (readCount < 0) { + return -1; + } + int total = 0; - return res; + if (count >= readBuffer.length) { + if (readCount > 0) { + System.arraycopy(readBuffer, readOffset, buffer, offset, readCount); + readOffset += readCount; + + offset += readCount; + count -= readCount; + + total = readCount; + readCount = 0; + } + total += Math.max(stream.read(buffer, offset, count), 0); + } else { + while (count > 0 && !fillBuffer()) { + int read = Math.min(readCount, count); + System.arraycopy(readBuffer, readOffset, buffer, offset, read); + + readOffset += read; + readCount -= read; + + offset += read; + count -= read; + + total += read; + } + } + + position += total; + return total; } - public final boolean available() { - return stream.available() > 0; + public boolean available() { + return readCount > 0 || stream.available() > 0; } public void rewind() throws IOException { stream.rewind(); - pos = 0; + + if ((position - viewSize) > 0) { + viewSize = 0;// drop view + } else { + viewSize += position; + } + + position = 0; + readOffset = readBuffer.length; } public boolean canRewind() { - return rewind; + return stream.canRewind(); } - private short[] primitive = new short[LONG_SIZE]; + /** + * Wraps this instance of {@code DataReader} into {@code InputStream} + * object. Note: Any read in the {@code DataReader} will not modify + * (decrease) the view size + * + * @param size the size of the view + * @return the view + */ + public InputStream getView(int size) { + if (view == null) { + view = new InputStream() { + @Override + public int read() throws IOException { + if (viewSize < 1) { + return -1; + } + int res = DataReader.this.read(); + if (res > 0) { + viewSize--; + } + return res; + } + + @Override + public int read(byte[] buffer) throws IOException { + return read(buffer, 0, buffer.length); + } + + @Override + public int read(byte[] buffer, int offset, int count) throws IOException { + if (viewSize < 1) { + return -1; + } + + int res = DataReader.this.read(buffer, offset, Math.min(viewSize, count)); + viewSize -= res; + + return res; + } + + @Override + public long skip(long amount) throws IOException { + if (viewSize < 1) { + return 0; + } + int res = (int) DataReader.this.skipBytes(Math.min(amount, viewSize)); + viewSize -= res; + + return res; + } + + @Override + public int available() { + return viewSize; + } + + @Override + public void close() { + viewSize = 0; + } + + @Override + public boolean markSupported() { + return false; + } + + }; + } + viewSize = size; + + return view; + } + + private final short[] primitive = new short[LONG_SIZE]; private void primitiveRead(int amount) throws IOException { byte[] buffer = new byte[amount]; - int read = stream.read(buffer, 0, amount); - pos += read; + int read = read(buffer, 0, amount); + if (read != amount) { - throw new EOFException("Truncated data, missing " + String.valueOf(amount - read) + " bytes"); + throw new EOFException("Truncated stream, missing " + String.valueOf(amount - read) + " bytes"); } - for (int i = 0; i < buffer.length; i++) { - primitive[i] = (short) (buffer[i] & 0xFF);// the "byte" datatype is signed and is very annoying + for (int i = 0; i < amount; i++) { + primitive[i] = (short) (buffer[i] & 0xFF);// the "byte" data type in java is signed and is very annoying } } + + private final byte[] readBuffer = new byte[8 * 1024]; + private int readOffset; + private int readCount; + + private boolean fillBuffer() throws IOException { + if (readCount < 0) { + return true; + } + if (readOffset >= readBuffer.length) { + readCount = stream.read(readBuffer); + if (readCount < 1) { + readCount = -1; + return true; + } + readOffset = 0; + } + + return readCount < 1; + } + } diff --git a/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java b/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java index 271929d47..c52ebf3aa 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java +++ b/app/src/main/java/org/schabi/newpipe/streams/Mp4DashReader.java @@ -1,17 +1,15 @@ package org.schabi.newpipe.streams; +import org.schabi.newpipe.streams.io.SharpStream; + import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; - import java.nio.ByteBuffer; - import java.util.ArrayList; import java.util.NoSuchElementException; -import org.schabi.newpipe.streams.io.SharpStream; - /** * @author kapodamy */ @@ -35,14 +33,29 @@ public class Mp4DashReader { private static final int ATOM_TREX = 0x74726578; private static final int ATOM_TKHD = 0x746B6864; private static final int ATOM_MFRA = 0x6D667261; - private static final int ATOM_TFRA = 0x74667261; private static final int ATOM_MDHD = 0x6D646864; + private static final int ATOM_EDTS = 0x65647473; + private static final int ATOM_ELST = 0x656C7374; + private static final int ATOM_HDLR = 0x68646C72; + private static final int ATOM_MINF = 0x6D696E66; + private static final int ATOM_DINF = 0x64696E66; + private static final int ATOM_STBL = 0x7374626C; + private static final int ATOM_STSD = 0x73747364; + private static final int ATOM_VMHD = 0x766D6864; + private static final int ATOM_SMHD = 0x736D6864; + private static final int BRAND_DASH = 0x64617368; + private static final int BRAND_ISO5 = 0x69736F35; + + private static final int HANDLER_VIDE = 0x76696465; + private static final int HANDLER_SOUN = 0x736F756E; + private static final int HANDLER_SUBT = 0x73756274; // private final DataReader stream; private Mp4Track[] tracks = null; + private int[] brands = null; private Box box; private Moof moof; @@ -50,9 +63,10 @@ public class Mp4DashReader { private boolean chunkZero = false; private int selectedTrack = -1; + private Box backupBox = null; public enum TrackKind { - Audio, Video, Other + Audio, Video, Subtitles, Other } public Mp4DashReader(SharpStream source) { @@ -65,8 +79,15 @@ public class Mp4DashReader { } box = readBox(ATOM_FTYP); - if (parse_ftyp() != BRAND_DASH) { - throw new NoSuchElementException("Main Brand is not dash"); + brands = parse_ftyp(box); + switch (brands[0]) { + case BRAND_DASH: + case BRAND_ISO5:// ¿why not? + break; + default: + throw new NoSuchElementException( + "Not a MPEG-4 DASH container, major brand is not 'dash' or 'iso5' is " + boxName(brands[0]) + ); } Moov moov = null; @@ -84,8 +105,6 @@ public class Mp4DashReader { break; case ATOM_MFRA: break; - case ATOM_MDAT: - throw new IOException("Expected moof, found mdat"); } } @@ -107,15 +126,26 @@ public class Mp4DashReader { } } - if (moov.trak[i].tkhd.bHeight == 0 && moov.trak[i].tkhd.bWidth == 0) { - tracks[i].kind = moov.trak[i].tkhd.bVolume == 0 ? TrackKind.Other : TrackKind.Audio; - } else { - tracks[i].kind = TrackKind.Video; + switch (moov.trak[i].mdia.hdlr.subType) { + case HANDLER_VIDE: + tracks[i].kind = TrackKind.Video; + break; + case HANDLER_SOUN: + tracks[i].kind = TrackKind.Audio; + break; + case HANDLER_SUBT: + tracks[i].kind = TrackKind.Subtitles; + break; + default: + tracks[i].kind = TrackKind.Other; + break; } } + + backupBox = box; } - public Mp4Track selectTrack(int index) { + Mp4Track selectTrack(int index) { selectedTrack = index; return tracks[index]; } @@ -126,7 +156,7 @@ public class Mp4DashReader { * @return list with a basic info * @throws IOException if the source stream is not seekeable */ - public int getFragmentsCount() throws IOException { + int getFragmentsCount() throws IOException { if (selectedTrack < 0) { throw new IllegalStateException("track no selected"); } @@ -136,7 +166,6 @@ public class Mp4DashReader { Box tmp; int count = 0; - long orig_offset = stream.position(); if (box.type == ATOM_MOOF) { tmp = box; @@ -162,17 +191,36 @@ public class Mp4DashReader { ensure(tmp); } while (stream.available() && (tmp = readBox()) != null); - stream.rewind(); - stream.skipBytes((int) orig_offset); + rewind(); return count; } + public int[] getBrands() { + if (brands == null) throw new IllegalStateException("Not parsed"); + return brands; + } + + public void rewind() throws IOException { + if (!stream.canRewind()) { + throw new IOException("The provided stream doesn't allow seek"); + } + if (box == null) { + return; + } + + box = backupBox; + chunkZero = false; + + stream.rewind(); + stream.skipBytes(backupBox.offset + (DataReader.INTEGER_SIZE * 2)); + } + public Mp4Track[] getAvailableTracks() { return tracks; } - public Mp4TrackChunk getNextChunk() throws IOException { + public Mp4DashChunk getNextChunk(boolean infoOnly) throws IOException { Mp4Track track = tracks[selectedTrack]; while (stream.available()) { @@ -208,7 +256,7 @@ public class Mp4DashReader { if (hasFlag(moof.traf.tfhd.bFlags, 0x10)) { moof.traf.trun.chunkSize = moof.traf.tfhd.defaultSampleSize * moof.traf.trun.entryCount; } else { - moof.traf.trun.chunkSize = box.size - 8; + moof.traf.trun.chunkSize = (int) (box.size - 8); } } if (!hasFlag(moof.traf.trun.bFlags, 0x900) && moof.traf.trun.chunkDuration == 0) { @@ -228,9 +276,12 @@ public class Mp4DashReader { continue;// find another chunk } - Mp4TrackChunk chunk = new Mp4TrackChunk(); + Mp4DashChunk chunk = new Mp4DashChunk(); chunk.moof = moof; - chunk.data = new TrackDataChunk(stream, moof.traf.trun.chunkSize); + if (!infoOnly) { + chunk.data = stream.getView(moof.traf.trun.chunkSize); + } + moof = null; stream.skipBytes(chunk.moof.traf.trun.dataOffset); @@ -269,6 +320,10 @@ public class Mp4DashReader { b.size = stream.readInt(); b.type = stream.readInt(); + if (b.size == 1) { + b.size = stream.readLong(); + } + return b; } @@ -280,6 +335,25 @@ public class Mp4DashReader { return b; } + private byte[] readFullBox(Box ref) throws IOException { + // full box reading is limited to 2 GiB, and should be enough + int size = (int) ref.size; + + ByteBuffer buffer = ByteBuffer.allocate(size); + buffer.putInt(size); + buffer.putInt(ref.type); + + int read = size - 8; + + if (stream.read(buffer.array(), 8, read) != read) { + throw new EOFException( + String.format("EOF reached in box: type=%s offset=%s size=%s", boxName(ref.type), ref.offset, ref.size) + ); + } + + return buffer.array(); + } + private void ensure(Box ref) throws IOException { long skip = ref.offset + ref.size - stream.position(); @@ -310,6 +384,14 @@ public class Mp4DashReader { return null; } + private Box untilAnyBox(Box ref) throws IOException { + if (stream.position() >= (ref.offset + ref.size)) { + return null; + } + + return readBox(); + } + // // @@ -329,7 +411,7 @@ public class Mp4DashReader { return obj; } } - + return obj; } @@ -397,14 +479,14 @@ public class Mp4DashReader { private long parse_tfdt() throws IOException { int version = stream.read(); - stream.skipBytes(3);// flags + stream.skipBytes(3);// flags return version == 0 ? readUint() : stream.readLong(); } private Trun parse_trun() throws IOException { Trun obj = new Trun(); obj.bFlags = stream.readInt(); - obj.entryCount = stream.readInt();// unsigned int + obj.entryCount = stream.readInt();// unsigned int obj.entries_rowSize = 0; if (hasFlag(obj.bFlags, 0x0100)) { @@ -448,11 +530,18 @@ public class Mp4DashReader { return obj; } - private int parse_ftyp() throws IOException { - int brand = stream.readInt(); + private int[] parse_ftyp(Box ref) throws IOException { + int i = 0; + int[] list = new int[(int) ((ref.offset + ref.size - stream.position() - 4) / 4)]; + + list[i++] = stream.readInt();// major brand + stream.skipBytes(4);// minor version - return brand; + for (; i < list.length; i++) + list[i] = stream.readInt();// compatible brands + + return list; } private Mvhd parse_mvhd() throws IOException { @@ -521,32 +610,66 @@ public class Mp4DashReader { trak.tkhd = parse_tkhd(); ensure(b); - b = untilBox(ref, ATOM_MDIA); - trak.mdia = new byte[b.size]; + while ((b = untilBox(ref, ATOM_MDIA, ATOM_EDTS)) != null) { + switch (b.type) { + case ATOM_MDIA: + trak.mdia = parse_mdia(b); + break; + case ATOM_EDTS: + trak.edst_elst = parse_edts(b); + break; + } - ByteBuffer buffer = ByteBuffer.wrap(trak.mdia); - buffer.putInt(b.size); - buffer.putInt(ATOM_MDIA); - stream.read(trak.mdia, 8, b.size - 8); - - trak.mdia_mdhd_timeScale = parse_mdia(buffer); + ensure(b); + } return trak; } - private int parse_mdia(ByteBuffer data) { - while (data.hasRemaining()) { - int end = data.position() + data.getInt(); - if (data.getInt() == ATOM_MDHD) { - byte version = data.get(); - data.position(data.position() + 3 + ((version == 0 ? 4 : 8) * 2)); - return data.getInt(); - } + private Mdia parse_mdia(Box ref) throws IOException { + Mdia obj = new Mdia(); - data.position(end); + Box b; + while ((b = untilBox(ref, ATOM_MDHD, ATOM_HDLR, ATOM_MINF)) != null) { + switch (b.type) { + case ATOM_MDHD: + obj.mdhd = readFullBox(b); + + // read time scale + ByteBuffer buffer = ByteBuffer.wrap(obj.mdhd); + byte version = buffer.get(8); + buffer.position(12 + ((version == 0 ? 4 : 8) * 2)); + obj.mdhd_timeScale = buffer.getInt(); + break; + case ATOM_HDLR: + obj.hdlr = parse_hdlr(b); + break; + case ATOM_MINF: + obj.minf = parse_minf(b); + break; + } + ensure(b); } - return 0;// this NEVER should happen + return obj; + } + + private Hdlr parse_hdlr(Box ref) throws IOException { + // version + // flags + stream.skipBytes(4); + + Hdlr obj = new Hdlr(); + obj.bReserved = new byte[12]; + + obj.type = stream.readInt(); + obj.subType = stream.readInt(); + stream.read(obj.bReserved); + + // component name (is a ansi/ascii string) + stream.skipBytes((ref.offset + ref.size) - stream.position()); + + return obj; } private Moov parse_moov(Box ref) throws IOException { @@ -570,7 +693,7 @@ public class Mp4DashReader { ensure(b); } - moov.trak = tmp.toArray(new Trak[tmp.size()]); + moov.trak = tmp.toArray(new Trak[0]); return moov; } @@ -584,7 +707,7 @@ public class Mp4DashReader { ensure(b); } - return tmp.toArray(new Trex[tmp.size()]); + return tmp.toArray(new Trex[0]); } private Trex parse_trex() throws IOException { @@ -602,74 +725,74 @@ public class Mp4DashReader { return obj; } - private Tfra parse_tfra() throws IOException { - int version = stream.read(); - - stream.skipBytes(3);// flags - - Tfra tfra = new Tfra(); - tfra.trackId = stream.readInt(); - - stream.skipBytes(3);// reserved - int bFlags = stream.read(); - int size_tts = ((bFlags >> 4) & 3) + ((bFlags >> 2) & 3) + (bFlags & 3); - - tfra.entries_time = new int[stream.readInt()]; - - for (int i = 0; i < tfra.entries_time.length; i++) { - tfra.entries_time[i] = version == 0 ? stream.readInt() : (int) stream.readLong(); - stream.skipBytes(size_tts + (version == 0 ? 4 : 8)); + private Elst parse_edts(Box ref) throws IOException { + Box b = untilBox(ref, ATOM_ELST); + if (b == null) { + return null; } - return tfra; - } - - private Sidx parse_sidx() throws IOException { - int version = stream.read(); + Elst obj = new Elst(); + boolean v1 = stream.read() == 1; stream.skipBytes(3);// flags - Sidx obj = new Sidx(); - obj.referenceId = stream.readInt(); - obj.timescale = stream.readInt(); + int entryCount = stream.readInt(); + if (entryCount < 1) { + obj.bMediaRate = 0x00010000;// default media rate (1.0) + return obj; + } - // earliest presentation entries_time - // first offset - // reserved - stream.skipBytes((2 * (version == 0 ? 4 : 8)) + 2); + if (v1) { + stream.skipBytes(DataReader.LONG_SIZE);// segment duration + obj.MediaTime = stream.readLong(); + // ignore all remain entries + stream.skipBytes((entryCount - 1) * (DataReader.LONG_SIZE * 2)); + } else { + stream.skipBytes(DataReader.INTEGER_SIZE);// segment duration + obj.MediaTime = stream.readInt(); + } - obj.entries_subsegmentDuration = new int[stream.readShort()]; + obj.bMediaRate = stream.readInt(); - for (int i = 0; i < obj.entries_subsegmentDuration.length; i++) { - // reference type - // referenced size - stream.skipBytes(4); - obj.entries_subsegmentDuration[i] = stream.readInt();// unsigned int + return obj; + } - // starts with SAP - // SAP type - // SAP delta entries_time - stream.skipBytes(4); + private Minf parse_minf(Box ref) throws IOException { + Minf obj = new Minf(); + + Box b; + while ((b = untilAnyBox(ref)) != null) { + + switch (b.type) { + case ATOM_DINF: + obj.dinf = readFullBox(b); + break; + case ATOM_STBL: + obj.stbl_stsd = parse_stbl(b); + break; + case ATOM_VMHD: + case ATOM_SMHD: + obj.$mhd = readFullBox(b); + break; + + } + ensure(b); } return obj; } - private Tfra[] parse_mfra(Box ref, int trackCount) throws IOException { - ArrayList tmp = new ArrayList<>(trackCount); - long limit = ref.offset + ref.size; + /** + * this only read the "stsd" box inside + */ + private byte[] parse_stbl(Box ref) throws IOException { + Box b = untilBox(ref, ATOM_STSD); - while (stream.position() < limit) { - box = readBox(); - - if (box.type == ATOM_TFRA) { - tmp.add(parse_tfra()); - } - - ensure(box); + if (b == null) { + return new byte[0];// this never should happens (missing codec startup data) } - return tmp.toArray(new Tfra[tmp.size()]); + return readFullBox(b); } // @@ -679,14 +802,7 @@ public class Mp4DashReader { int type; long offset; - int size; - } - - class Sidx { - - int timescale; - int referenceId; - int[] entries_subsegmentDuration; + long size; } public class Moof { @@ -711,12 +827,16 @@ public class Mp4DashReader { int defaultSampleFlags; } - public class TrunEntry { + class TrunEntry { + + int sampleDuration; + int sampleSize; + int sampleFlags; + int sampleCompositionTimeOffset; + + boolean hasCompositionTimeOffset; + boolean isKeyframe; - public int sampleDuration; - public int sampleSize; - public int sampleFlags; - public int sampleCompositionTimeOffset; } public class Trun { @@ -749,6 +869,31 @@ public class Mp4DashReader { entry.sampleCompositionTimeOffset = buffer.getInt(); } + entry.hasCompositionTimeOffset = hasFlag(bFlags, 0x0800); + entry.isKeyframe = !hasFlag(entry.sampleFlags, 0x10000); + + return entry; + } + + public TrunEntry getAbsoluteEntry(int i, Tfhd header) { + TrunEntry entry = getEntry(i); + + if (!hasFlag(bFlags, 0x0100) && hasFlag(header.bFlags, 0x20)) { + entry.sampleFlags = header.defaultSampleFlags; + } + + if (!hasFlag(bFlags, 0x0200) && hasFlag(header.bFlags, 0x10)) { + entry.sampleSize = header.defaultSampleSize; + } + + if (!hasFlag(bFlags, 0x0100) && hasFlag(header.bFlags, 0x08)) { + entry.sampleDuration = header.defaultSampleDuration; + } + + if (i == 0 && hasFlag(bFlags, 0x0004)) { + entry.sampleFlags = bFirstSampleFlags; + } + return entry; } } @@ -768,9 +913,9 @@ public class Mp4DashReader { public class Trak { public Tkhd tkhd; - public int mdia_mdhd_timeScale; + public Elst edst_elst; + public Mdia mdia; - byte[] mdia; } class Mvhd { @@ -786,12 +931,6 @@ public class Mp4DashReader { Trex[] mvex_trex; } - class Tfra { - - int trackId; - int[] entries_time; - } - public class Trex { private int trackId; @@ -801,6 +940,34 @@ public class Mp4DashReader { int defaultSampleFlags; } + public class Elst { + + public long MediaTime; + public int bMediaRate; + } + + public class Mdia { + + public int mdhd_timeScale; + public byte[] mdhd; + public Hdlr hdlr; + public Minf minf; + } + + public class Hdlr { + + public int type; + public int subType; + public byte[] bReserved; + } + + public class Minf { + + public byte[] dinf; + public byte[] stbl_stsd; + public byte[] $mhd; + } + public class Mp4Track { public TrackKind kind; @@ -808,10 +975,43 @@ public class Mp4DashReader { public Trex trex; } - public class Mp4TrackChunk { + public class Mp4DashChunk { public InputStream data; public Moof moof; + private int i = 0; + + public TrunEntry getNextSampleInfo() { + if (i >= moof.traf.trun.entryCount) { + return null; + } + return moof.traf.trun.getAbsoluteEntry(i++, moof.traf.tfhd); + } + + public Mp4DashSample getNextSample() throws IOException { + if (data == null) { + throw new IllegalStateException("This chunk has info only"); + } + if (i >= moof.traf.trun.entryCount) { + return null; + } + + Mp4DashSample sample = new Mp4DashSample(); + sample.info = moof.traf.trun.getAbsoluteEntry(i++, moof.traf.tfhd); + sample.data = new byte[sample.info.sampleSize]; + + if (data.read(sample.data) != sample.info.sampleSize) { + throw new EOFException("EOF reached while reading a sample"); + } + + return sample; + } + } + + public class Mp4DashSample { + + public TrunEntry info; + public byte[] data; } // } diff --git a/app/src/main/java/org/schabi/newpipe/streams/Mp4DashWriter.java b/app/src/main/java/org/schabi/newpipe/streams/Mp4DashWriter.java deleted file mode 100644 index babb2e24c..000000000 --- a/app/src/main/java/org/schabi/newpipe/streams/Mp4DashWriter.java +++ /dev/null @@ -1,623 +0,0 @@ -package org.schabi.newpipe.streams; - -import org.schabi.newpipe.streams.io.SharpStream; - -import org.schabi.newpipe.streams.Mp4DashReader.Mp4Track; -import org.schabi.newpipe.streams.Mp4DashReader.Mp4TrackChunk; -import org.schabi.newpipe.streams.Mp4DashReader.Trak; -import org.schabi.newpipe.streams.Mp4DashReader.Trex; - - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; - -import static org.schabi.newpipe.streams.Mp4DashReader.hasFlag; - -/** - * - * @author kapodamy - */ -public class Mp4DashWriter { - - private final static byte DIMENSIONAL_FIVE = 5; - private final static byte DIMENSIONAL_TWO = 2; - private final static short DEFAULT_TIMESCALE = 1000; - private final static int BUFFER_SIZE = 8 * 1024; - private final static byte DEFAULT_TREX_SIZE = 32; - private final static byte[] TFRA_TTS_DEFAULT = new byte[]{0x01, 0x01, 0x01}; - private final static int EPOCH_OFFSET = 2082844800; - - private Mp4Track[] infoTracks; - private SharpStream[] sourceTracks; - - private Mp4DashReader[] readers; - private final long time; - - private boolean done = false; - private boolean parsed = false; - - private long written = 0; - private ArrayList> chunkTimes; - private ArrayList moofOffsets; - private ArrayList fragSizes; - - public Mp4DashWriter(SharpStream... source) { - sourceTracks = source; - readers = new Mp4DashReader[sourceTracks.length]; - infoTracks = new Mp4Track[sourceTracks.length]; - time = (System.currentTimeMillis() / 1000L) + EPOCH_OFFSET; - } - - public Mp4Track[] getTracksFromSource(int sourceIndex) throws IllegalStateException { - if (!parsed) { - throw new IllegalStateException("All sources must be parsed first"); - } - - return readers[sourceIndex].getAvailableTracks(); - } - - public void parseSources() throws IOException, IllegalStateException { - if (done) { - throw new IllegalStateException("already done"); - } - if (parsed) { - throw new IllegalStateException("already parsed"); - } - - try { - for (int i = 0; i < readers.length; i++) { - readers[i] = new Mp4DashReader(sourceTracks[i]); - readers[i].parse(); - } - - } finally { - parsed = true; - } - } - - public void selectTracks(int... trackIndex) throws IOException { - if (done) { - throw new IOException("already done"); - } - if (chunkTimes != null) { - throw new IOException("tracks already selected"); - } - - try { - chunkTimes = new ArrayList<>(readers.length); - moofOffsets = new ArrayList<>(32); - fragSizes = new ArrayList<>(32); - - for (int i = 0; i < readers.length; i++) { - infoTracks[i] = readers[i].selectTrack(trackIndex[i]); - - chunkTimes.add(new ArrayList(32)); - } - - } finally { - parsed = true; - } - } - - public long getBytesWritten() { - return written; - } - - public void build(SharpStream out) throws IOException, RuntimeException { - if (done) { - throw new RuntimeException("already done"); - } - if (!out.canWrite()) { - throw new IOException("the provided output is not writable"); - } - - long sidxOffsets = -1; - int maxFrags = 0; - - for (SharpStream stream : sourceTracks) { - if (!stream.canRewind()) { - sidxOffsets = -2;// sidx not available - } - } - - try { - dump(make_ftyp(), out); - dump(make_moov(), out); - - if (sidxOffsets == -1 && out.canRewind()) { - // - int reserved = 0; - for (Mp4DashReader reader : readers) { - int count = reader.getFragmentsCount(); - if (count > maxFrags) { - maxFrags = count; - } - reserved += 12 + calcSidxBodySize(count); - } - if (maxFrags > 0xFFFF) { - sidxOffsets = -3;// TODO: to many fragments, needs a multi-sidx implementation - } else { - sidxOffsets = written; - dump(make_free(reserved), out); - } - // - } - ArrayList chunks = new ArrayList<>(readers.length); - chunks.add(null); - - int read; - byte[] buffer = new byte[BUFFER_SIZE]; - int sequenceNumber = 1; - - while (true) { - chunks.clear(); - - for (int i = 0; i < readers.length; i++) { - Mp4TrackChunk chunk = readers[i].getNextChunk(); - if (chunk == null || chunk.moof.traf.trun.chunkSize < 1) { - continue; - } - chunk.moof.traf.tfhd.trackId = i + 1; - chunks.add(chunk); - - if (sequenceNumber == 1) { - if (chunk.moof.traf.trun.entryCount > 0 && hasFlag(chunk.moof.traf.trun.bFlags, 0x0800)) { - chunkTimes.get(i).add(chunk.moof.traf.trun.getEntry(0).sampleCompositionTimeOffset); - } else { - chunkTimes.get(i).add(0); - } - } - - chunkTimes.get(i).add(chunk.moof.traf.trun.chunkDuration); - } - - if (chunks.size() < 1) { - break; - } - - long offset = written; - moofOffsets.add(offset); - - dump(make_moof(sequenceNumber++, chunks, offset), out); - dump(make_mdat(chunks), out); - - for (Mp4TrackChunk chunk : chunks) { - while ((read = chunk.data.read(buffer)) > 0) { - out.write(buffer, 0, read); - written += read; - } - } - - fragSizes.add((int) (written - offset)); - } - - dump(make_mfra(), out); - - if (sidxOffsets > 0 && moofOffsets.size() == maxFrags) { - long len = written; - - out.rewind(); - out.skip(sidxOffsets); - - written = sidxOffsets; - sidxOffsets = moofOffsets.get(0); - - for (int i = 0; i < readers.length; i++) { - dump(make_sidx(i, sidxOffsets - written), out); - } - - written = len; - } - } finally { - done = true; - } - } - - public boolean isDone() { - return done; - } - - public boolean isParsed() { - return parsed; - } - - public void close() { - done = true; - parsed = true; - - for (SharpStream src : sourceTracks) { - src.dispose(); - } - - sourceTracks = null; - readers = null; - infoTracks = null; - moofOffsets = null; - chunkTimes = null; - } - - // - private void dump(byte[][] buffer, SharpStream stream) throws IOException { - for (byte[] buff : buffer) { - stream.write(buff); - written += buff.length; - } - } - - private byte[][] lengthFor(byte[][] buffer) { - int length = 0; - for (byte[] buff : buffer) { - length += buff.length; - } - - ByteBuffer.wrap(buffer[0]).putInt(length); - - return buffer; - } - - private int calcSidxBodySize(int entryCount) { - return 4 + 4 + 8 + 8 + 4 + (entryCount * 12); - } - // - - // - private byte[][] make_moof(int sequence, ArrayList chunks, long referenceOffset) { - int pos = 2; - TrunExtra[] extra = new TrunExtra[chunks.size()]; - - byte[][] buffer = new byte[pos + (extra.length * DIMENSIONAL_FIVE)][]; - buffer[0] = new byte[]{ - 0x00, 0x00, 0x00, 0x00, 0x6D, 0x6F, 0x6F, 0x66,// info header - 0x00, 0x00, 0x00, 0x10, 0x6D, 0x66, 0x68, 0x64, 0x00, 0x00, 0x00, 0x00//mfhd - }; - buffer[1] = new byte[4]; - ByteBuffer.wrap(buffer[1]).putInt(sequence); - - for (int i = 0; i < extra.length; i++) { - extra[i] = new TrunExtra(); - for (byte[] buff : make_traf(chunks.get(i), extra[i], referenceOffset)) { - buffer[pos++] = buff; - } - } - - lengthFor(buffer); - - int offset = 8 + ByteBuffer.wrap(buffer[0]).getInt(); - - for (int i = 0; i < extra.length; i++) { - extra[i].byteBuffer.putInt(offset); - offset += chunks.get(i).moof.traf.trun.chunkSize; - } - - return buffer; - } - - private byte[][] make_traf(Mp4TrackChunk chunk, TrunExtra extra, long moofOffset) { - byte[][] buffer = new byte[DIMENSIONAL_FIVE][]; - buffer[0] = new byte[]{ - 0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x61, 0x66, - 0x00, 0x00, 0x00, 0x00, 0x74, 0x66, 0x68, 0x64 - }; - - int flags = (chunk.moof.traf.tfhd.bFlags & 0x38) | 0x01; - byte tfhdBodySize = 8 + 8; - if (hasFlag(flags, 0x08)) { - tfhdBodySize += 4; - } - if (hasFlag(flags, 0x10)) { - tfhdBodySize += 4; - } - if (hasFlag(flags, 0x20)) { - tfhdBodySize += 4; - } - buffer[1] = new byte[tfhdBodySize]; - ByteBuffer set = ByteBuffer.wrap(buffer[1]); - set.position(4); - set.putInt(chunk.moof.traf.tfhd.trackId); - set.putLong(moofOffset); - if (hasFlag(flags, 0x08)) { - set.putInt(chunk.moof.traf.tfhd.defaultSampleDuration); - } - if (hasFlag(flags, 0x10)) { - set.putInt(chunk.moof.traf.tfhd.defaultSampleSize); - } - if (hasFlag(flags, 0x20)) { - set.putInt(chunk.moof.traf.tfhd.defaultSampleFlags); - } - set.putInt(0, flags); - ByteBuffer.wrap(buffer[0]).putInt(8, 8 + tfhdBodySize); - - buffer[2] = new byte[]{ - 0x00, 0x00, 0x00, 0x14, - 0x74, 0x66, 0x64, 0x74, - 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00 - }; - - ByteBuffer.wrap(buffer[2]).putLong(12, chunk.moof.traf.tfdt); - - buffer[3] = new byte[]{ - 0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x75, 0x6E, - 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00 - }; - - buffer[4] = chunk.moof.traf.trun.bEntries; - - lengthFor(buffer); - - set = ByteBuffer.wrap(buffer[3]); - set.putInt(buffer[3].length + buffer[4].length); - set.position(8); - set.putInt((chunk.moof.traf.trun.bFlags | 0x01) & 0x0F01); - set.putInt(chunk.moof.traf.trun.entryCount); - extra.byteBuffer = set; - - return buffer; - } - - private byte[][] make_mdat(ArrayList chunks) { - byte[][] buffer = new byte[][]{ - { - 0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x61, 0x74 - } - }; - - int length = 0; - - for (Mp4TrackChunk chunk : chunks) { - length += chunk.moof.traf.trun.chunkSize; - } - - ByteBuffer.wrap(buffer[0]).putInt(length + 8); - - return buffer; - } - - private byte[][] make_ftyp() { - return new byte[][]{ - { - 0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, 0x64, 0x61, 0x73, 0x68, 0x00, 0x00, 0x00, 0x00, - 0x6D, 0x70, 0x34, 0x31, 0x69, 0x73, 0x6F, 0x6D, 0x69, 0x73, 0x6F, 0x36, 0x69, 0x73, 0x6F, 0x32 - } - }; - } - - private byte[][] make_mvhd() { - byte[][] buffer = new byte[DIMENSIONAL_FIVE][]; - - buffer[0] = new byte[]{ - 0x00, 0x00, 0x00, 0x78, 0x6D, 0x76, 0x68, 0x64, 0x01, 0x00, 0x00, 0x00 - }; - buffer[1] = new byte[28]; - buffer[2] = new byte[]{ - 0x00, 0x01, 0x00, 0x00, 0x01, 0x00,// default volume and rate - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,// reserved values - // default matrix - 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x40, 0x00, 0x00, 0x00 - }; - buffer[3] = new byte[24];// predefined - buffer[4] = ByteBuffer.allocate(4).putInt(infoTracks.length + 1).array(); - - long longestTrack = 0; - - for (Mp4Track track : infoTracks) { - long tmp = (long) ((track.trak.tkhd.duration / (double) track.trak.mdia_mdhd_timeScale) * DEFAULT_TIMESCALE); - if (tmp > longestTrack) { - longestTrack = tmp; - } - } - - ByteBuffer.wrap(buffer[1]) - .putLong(time) - .putLong(time) - .putInt(DEFAULT_TIMESCALE) - .putLong(longestTrack); - - return buffer; - } - - private byte[][] make_trak(int trackId, Trak trak) throws RuntimeException { - if (trak.tkhd.matrix.length != 36) { - throw new RuntimeException("bad track matrix length (expected 36)"); - } - - byte[][] buffer = new byte[DIMENSIONAL_FIVE][]; - - buffer[0] = new byte[]{ - 0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x61, 0x6B,// trak header - 0x00, 0x00, 0x00, 0x68, 0x74, 0x6B, 0x68, 0x64, 0x01, 0x00, 0x00, 0x03 // tkhd header - }; - buffer[1] = new byte[48]; - buffer[2] = trak.tkhd.matrix; - buffer[3] = new byte[8]; - buffer[4] = trak.mdia; - - ByteBuffer set = ByteBuffer.wrap(buffer[1]); - set.putLong(time); - set.putLong(time); - set.putInt(trackId); - set.position(24); - set.putLong(trak.tkhd.duration); - set.position(40); - set.putShort(trak.tkhd.bLayer); - set.putShort(trak.tkhd.bAlternateGroup); - set.putShort(trak.tkhd.bVolume); - - ByteBuffer.wrap(buffer[3]) - .putInt(trak.tkhd.bWidth) - .putInt(trak.tkhd.bHeight); - - return lengthFor(buffer); - } - - private byte[][] make_moov() throws RuntimeException { - int pos = 1; - byte[][] buffer = new byte[2 + (DIMENSIONAL_TWO * infoTracks.length) + (DIMENSIONAL_FIVE * infoTracks.length) + DIMENSIONAL_FIVE + 1][]; - - buffer[0] = new byte[]{ - 0x00, 0x00, 0x00, 0x00, 0x6D, 0x6F, 0x6F, 0x76 - }; - - for (byte[] buff : make_mvhd()) { - buffer[pos++] = buff; - } - - for (int i = 0; i < infoTracks.length; i++) { - for (byte[] buff : make_trak(i + 1, infoTracks[i].trak)) { - buffer[pos++] = buff; - } - } - - buffer[pos] = new byte[]{ - 0x00, 0x00, 0x00, 0x00, 0x6D, 0x76, 0x65, 0x78 - }; - - ByteBuffer.wrap(buffer[pos++]).putInt((infoTracks.length * DEFAULT_TREX_SIZE) + 8); - - for (int i = 0; i < infoTracks.length; i++) { - for (byte[] buff : make_trex(i + 1, infoTracks[i].trex)) { - buffer[pos++] = buff; - } - } - - // default udta - buffer[pos] = new byte[]{ - 0x00, 0x00, 0x00, 0x5C, 0x75, 0x64, 0x74, 0x61, 0x00, 0x00, 0x00, 0x54, 0x6D, 0x65, 0x74, 0x61, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x68, 0x64, 0x6C, 0x72, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x72, 0x61, 0x70, 0x70, 0x6C, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0x69, 0x6C, 0x73, 0x74, 0x00, 0x00, 0x00, - 0x1F, (byte) 0xA9, 0x63, 0x6D, 0x74, 0x00, 0x00, 0x00, 0x17, 0x64, 0x61, 0x74, 0x61, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, - 0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string - }; - - return lengthFor(buffer); - } - - private byte[][] make_trex(int trackId, Trex trex) { - byte[][] buffer = new byte[][]{ - { - 0x00, 0x00, 0x00, 0x20, 0x74, 0x72, 0x65, 0x78, 0x00, 0x00, 0x00, 0x00 - }, - new byte[20] - }; - - ByteBuffer.wrap(buffer[1]) - .putInt(trackId) - .putInt(trex.defaultSampleDescriptionIndex) - .putInt(trex.defaultSampleDuration) - .putInt(trex.defaultSampleSize) - .putInt(trex.defaultSampleFlags); - - return buffer; - } - - private byte[][] make_tfra(int trackId, List times, List moofOffsets) { - int entryCount = times.size() - 1; - byte[][] buffer = new byte[DIMENSIONAL_TWO][]; - buffer[0] = new byte[]{ - 0x00, 0x00, 0x00, 0x00, 0x74, 0x66, 0x72, 0x61, 0x01, 0x00, 0x00, 0x00 - }; - buffer[1] = new byte[12 + ((16 + TFRA_TTS_DEFAULT.length) * entryCount)]; - - ByteBuffer set = ByteBuffer.wrap(buffer[1]); - set.putInt(trackId); - set.position(8); - set.putInt(entryCount); - - long decodeTime = 0; - - for (int i = 0; i < entryCount; i++) { - decodeTime += times.get(i); - set.putLong(decodeTime); - set.putLong(moofOffsets.get(i)); - set.put(TFRA_TTS_DEFAULT);// default values: traf number/trun number/sample number - } - - return lengthFor(buffer); - } - - private byte[][] make_mfra() { - byte[][] buffer = new byte[2 + (DIMENSIONAL_TWO * infoTracks.length)][]; - buffer[0] = new byte[]{ - 0x00, 0x00, 0x00, 0x00, 0x6D, 0x66, 0x72, 0x61 - }; - int pos = 1; - - for (int i = 0; i < infoTracks.length; i++) { - for (byte[] buff : make_tfra(i + 1, chunkTimes.get(i), moofOffsets)) { - buffer[pos++] = buff; - } - } - - buffer[pos] = new byte[]{// mfro - 0x00, 0x00, 0x00, 0x10, 0x6D, 0x66, 0x72, 0x6F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - }; - - lengthFor(buffer); - - ByteBuffer set = ByteBuffer.wrap(buffer[pos]); - set.position(12); - set.put(buffer[0], 0, 4); - - return buffer; - - } - - private byte[][] make_sidx(int internalTrackId, long firstOffset) { - List times = chunkTimes.get(internalTrackId); - int count = times.size() - 1;// the first item is ignored (composition time) - - if (count > 65535) { - throw new OutOfMemoryError("to many fragments. sidx limit is 65535, found " + String.valueOf(count)); - } - - byte[][] buffer = new byte[][]{ - new byte[]{ - 0x00, 0x00, 0x00, 0x00, 0x73, 0x69, 0x64, 0x78, 0x01, 0x00, 0x00, 0x00 - }, - new byte[calcSidxBodySize(count)] - }; - - lengthFor(buffer); - - ByteBuffer set = ByteBuffer.wrap(buffer[1]); - set.putInt(internalTrackId + 1); - set.putInt(infoTracks[internalTrackId].trak.mdia_mdhd_timeScale); - set.putLong(0); - set.putLong(firstOffset - ByteBuffer.wrap(buffer[0]).getInt()); - set.putInt(0xFFFF & count);// unsigned - - int i = 0; - while (i < count) { - set.putInt(fragSizes.get(i) & 0x7fffffff);// default reference type is 0 - set.putInt(times.get(i + 1)); - set.putInt(0x90000000);// default SAP settings - i++; - } - - return buffer; - } - - private byte[][] make_free(int totalSize) { - return lengthFor(new byte[][]{ - new byte[]{0x00, 0x00, 0x00, 0x00, 0x66, 0x72, 0x65, 0x65}, - new byte[totalSize - 8]// this is waste of RAM - }); - - } - -// - - class TrunExtra { - - ByteBuffer byteBuffer; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java b/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java new file mode 100644 index 000000000..5a4efbe32 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java @@ -0,0 +1,810 @@ +package org.schabi.newpipe.streams; + +import org.schabi.newpipe.streams.Mp4DashReader.Hdlr; +import org.schabi.newpipe.streams.Mp4DashReader.Mdia; +import org.schabi.newpipe.streams.Mp4DashReader.Mp4DashChunk; +import org.schabi.newpipe.streams.Mp4DashReader.Mp4DashSample; +import org.schabi.newpipe.streams.Mp4DashReader.Mp4Track; +import org.schabi.newpipe.streams.Mp4DashReader.TrunEntry; +import org.schabi.newpipe.streams.io.SharpStream; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * + * @author kapodamy + */ +public class Mp4FromDashWriter { + + private final static int EPOCH_OFFSET = 2082844800; + private final static short DEFAULT_TIMESCALE = 1000; + private final static byte SAMPLES_PER_CHUNK_INIT = 2; + private final static byte SAMPLES_PER_CHUNK = 6;// ffmpeg uses 2, basic uses 1 (with 60fps uses 21 or 22). NewPipe will use 6 + private final static long THRESHOLD_FOR_CO64 = 0xFFFEFFFFL;// near 3.999 GiB + private final static int THRESHOLD_MOOV_LENGTH = (256 * 1024) + (2048 * 1024); // 2.2 MiB enough for: 1080p 60fps 00h35m00s + + private final long time; + + private ByteBuffer auxBuffer; + private SharpStream outStream; + + private long lastWriteOffset = -1; + private long writeOffset; + + private boolean moovSimulation = true; + + private boolean done = false; + private boolean parsed = false; + + private Mp4Track[] tracks; + private SharpStream[] sourceTracks; + + private Mp4DashReader[] readers; + private Mp4DashChunk[] readersChunks; + + private int overrideMainBrand = 0x00; + + public Mp4FromDashWriter(SharpStream... sources) throws IOException { + for (SharpStream src : sources) { + if (!src.canRewind() && !src.canRead()) { + throw new IOException("All sources must be readable and allow rewind"); + } + } + + sourceTracks = sources; + readers = new Mp4DashReader[sourceTracks.length]; + readersChunks = new Mp4DashChunk[readers.length]; + time = (System.currentTimeMillis() / 1000L) + EPOCH_OFFSET; + } + + public Mp4Track[] getTracksFromSource(int sourceIndex) throws IllegalStateException { + if (!parsed) { + throw new IllegalStateException("All sources must be parsed first"); + } + + return readers[sourceIndex].getAvailableTracks(); + } + + public void parseSources() throws IOException, IllegalStateException { + if (done) { + throw new IllegalStateException("already done"); + } + if (parsed) { + throw new IllegalStateException("already parsed"); + } + + try { + for (int i = 0; i < readers.length; i++) { + readers[i] = new Mp4DashReader(sourceTracks[i]); + readers[i].parse(); + } + + } finally { + parsed = true; + } + } + + public void selectTracks(int... trackIndex) throws IOException { + if (done) { + throw new IOException("already done"); + } + if (tracks != null) { + throw new IOException("tracks already selected"); + } + + try { + tracks = new Mp4Track[readers.length]; + for (int i = 0; i < readers.length; i++) { + tracks[i] = readers[i].selectTrack(trackIndex[i]); + } + } finally { + parsed = true; + } + } + + public void setMainBrand(int brandId) { + overrideMainBrand = brandId; + } + + public boolean isDone() { + return done; + } + + public boolean isParsed() { + return parsed; + } + + public void close() throws IOException { + done = true; + parsed = true; + + for (SharpStream src : sourceTracks) { + src.dispose(); + } + + tracks = null; + sourceTracks = null; + + readers = null; + readersChunks = null; + + auxBuffer = null; + outStream = null; + } + + public void build(SharpStream output) throws IOException { + if (done) { + throw new RuntimeException("already done"); + } + if (!output.canWrite()) { + throw new IOException("the provided output is not writable"); + } + + // + // WARNING: the muxer requires at least 8 samples of every track + // not allowed for very short tracks (less than 0.5 seconds) + // + outStream = output; + int read = 8;// mdat box header size + long totalSampleSize = 0; + int[] sampleExtra = new int[readers.length]; + int[] defaultMediaTime = new int[readers.length]; + int[] defaultSampleDuration = new int[readers.length]; + int[] sampleCount = new int[readers.length]; + + TablesInfo[] tablesInfo = new TablesInfo[tracks.length]; + for (int i = 0; i < tablesInfo.length; i++) { + tablesInfo[i] = new TablesInfo(); + } + + // + for (int i = 0; i < readers.length; i++) { + int samplesSize = 0; + int sampleSizeChanges = 0; + int compositionOffsetLast = -1; + + Mp4DashChunk chunk; + while ((chunk = readers[i].getNextChunk(true)) != null) { + + if (defaultMediaTime[i] < 1 && chunk.moof.traf.tfhd.defaultSampleDuration > 0) { + defaultMediaTime[i] = chunk.moof.traf.tfhd.defaultSampleDuration; + } + + read += chunk.moof.traf.trun.chunkSize; + sampleExtra[i] += chunk.moof.traf.trun.chunkDuration;// calculate track duration + + TrunEntry info; + while ((info = chunk.getNextSampleInfo()) != null) { + if (info.isKeyframe) { + tablesInfo[i].stss++; + } + + if (info.sampleDuration > defaultSampleDuration[i]) { + defaultSampleDuration[i] = info.sampleDuration; + } + + tablesInfo[i].stsz++; + if (samplesSize != info.sampleSize) { + samplesSize = info.sampleSize; + sampleSizeChanges++; + } + + if (info.hasCompositionTimeOffset) { + if (info.sampleCompositionTimeOffset != compositionOffsetLast) { + tablesInfo[i].ctts++; + compositionOffsetLast = info.sampleCompositionTimeOffset; + } + } + + totalSampleSize += info.sampleSize; + } + } + + if (defaultMediaTime[i] < 1) { + defaultMediaTime[i] = defaultSampleDuration[i]; + } + + readers[i].rewind(); + + int tmp = tablesInfo[i].stsz - SAMPLES_PER_CHUNK_INIT; + tablesInfo[i].stco = (tmp / SAMPLES_PER_CHUNK) + 1;// +1 for samples in first chunk + + tmp = tmp % SAMPLES_PER_CHUNK; + if (tmp == 0) { + tablesInfo[i].stsc = 2;// first chunk (init) and succesive chunks + tablesInfo[i].stsc_bEntries = new int[]{ + 1, SAMPLES_PER_CHUNK_INIT, 1, + 2, SAMPLES_PER_CHUNK, 1 + }; + } else { + tablesInfo[i].stsc = 3;// first chunk (init) and succesive chunks and remain chunk + tablesInfo[i].stsc_bEntries = new int[]{ + 1, SAMPLES_PER_CHUNK_INIT, 1, + 2, SAMPLES_PER_CHUNK, 1, + tablesInfo[i].stco + 1, tmp, 1 + }; + tablesInfo[i].stco++; + } + + sampleCount[i] = tablesInfo[i].stsz; + + if (sampleSizeChanges == 1) { + tablesInfo[i].stsz = 0; + tablesInfo[i].stsz_default = samplesSize; + } else { + tablesInfo[i].stsz_default = 0; + } + + if (tablesInfo[i].stss == tablesInfo[i].stsz) { + tablesInfo[i].stss = -1;// for audio tracks (all samples are keyframes) + } + + // ensure track duration + if (tracks[i].trak.tkhd.duration < 1) { + tracks[i].trak.tkhd.duration = sampleExtra[i];// this never should happen + } + } + // + + boolean is64 = read > THRESHOLD_FOR_CO64; + + // calculate the moov size; + int auxSize = make_moov(defaultMediaTime, tablesInfo, is64); + + if (auxSize < THRESHOLD_MOOV_LENGTH) { + auxBuffer = ByteBuffer.allocate(auxSize);// cache moov in the memory + } + + moovSimulation = false; + writeOffset = 0; + + final int ftyp_size = make_ftyp(); + + // reserve moov space in the output stream + if (outStream.canSetLength()) { + long length = writeOffset + auxSize; + outStream.setLength(length); + outSeek(length); + } else { + // hard way + int length = auxSize; + byte[] buffer = new byte[8 * 1024];// 8 KiB + while (length > 0) { + int count = Math.min(length, buffer.length); + outWrite(buffer, 0, count); + length -= count; + } + } + if (auxBuffer == null) { + outSeek(ftyp_size); + } + + // tablesInfo contais row counts + // and after returning from make_moov() will contain table offsets + make_moov(defaultMediaTime, tablesInfo, is64); + + // write tables: stts stsc + // reset for ctts table: sampleCount sampleExtra + for (int i = 0; i < readers.length; i++) { + writeEntryArray(tablesInfo[i].stts, 2, sampleCount[i], defaultSampleDuration[i]); + writeEntryArray(tablesInfo[i].stsc, tablesInfo[i].stsc_bEntries.length, tablesInfo[i].stsc_bEntries); + tablesInfo[i].stsc_bEntries = null; + if (tablesInfo[i].ctts > 0) { + sampleCount[i] = 1;// index is not base zero + sampleExtra[i] = -1; + } + } + + if (auxBuffer == null) { + outRestore(); + } + + outWrite(make_mdat(totalSampleSize, is64)); + + int[] sampleIndex = new int[readers.length]; + int[] sizes = new int[SAMPLES_PER_CHUNK]; + int[] sync = new int[SAMPLES_PER_CHUNK]; + + int written = readers.length; + while (written > 0) { + written = 0; + + for (int i = 0; i < readers.length; i++) { + if (sampleIndex[i] < 0) { + continue;// track is done + } + + long chunkOffset = writeOffset; + int syncCount = 0; + int limit = sampleIndex[i] == 0 ? SAMPLES_PER_CHUNK_INIT : SAMPLES_PER_CHUNK; + + int j = 0; + for (; j < limit; j++) { + Mp4DashSample sample = getNextSample(i); + + if (sample == null) { + if (tablesInfo[i].ctts > 0 && sampleExtra[i] >= 0) { + writeEntryArray(tablesInfo[i].ctts, 1, sampleCount[i], sampleExtra[i]);// flush last entries + } + sampleIndex[i] = -1; + break; + } + + sampleIndex[i]++; + + if (tablesInfo[i].ctts > 0) { + if (sample.info.sampleCompositionTimeOffset == sampleExtra[i]) { + sampleCount[i]++; + } else { + if (sampleExtra[i] >= 0) { + tablesInfo[i].ctts = writeEntryArray(tablesInfo[i].ctts, 2, sampleCount[i], sampleExtra[i]); + outRestore(); + } + sampleCount[i] = 1; + sampleExtra[i] = sample.info.sampleCompositionTimeOffset; + } + } + + if (tablesInfo[i].stss > 0 && sample.info.isKeyframe) { + sync[syncCount++] = sampleIndex[i]; + } + + if (tablesInfo[i].stsz > 0) { + sizes[j] = sample.data.length; + } + + outWrite(sample.data, 0, sample.data.length); + } + + if (j > 0) { + written++; + + if (tablesInfo[i].stsz > 0) { + tablesInfo[i].stsz = writeEntryArray(tablesInfo[i].stsz, j, sizes); + } + + if (syncCount > 0) { + tablesInfo[i].stss = writeEntryArray(tablesInfo[i].stss, syncCount, sync); + } + + if (is64) { + tablesInfo[i].stco = writeEntry64(tablesInfo[i].stco, chunkOffset); + } else { + tablesInfo[i].stco = writeEntryArray(tablesInfo[i].stco, 1, (int) chunkOffset); + } + + outRestore(); + } + } + } + + if (auxBuffer != null) { + // dump moov + outSeek(ftyp_size); + outStream.write(auxBuffer.array(), 0, auxBuffer.capacity()); + auxBuffer = null; + } + } + + private Mp4DashSample getNextSample(int track) throws IOException { + if (readersChunks[track] == null) { + readersChunks[track] = readers[track].getNextChunk(false); + if (readersChunks[track] == null) { + return null;// EOF reached + } + } + + Mp4DashSample sample = readersChunks[track].getNextSample(); + if (sample == null) { + readersChunks[track] = null; + return getNextSample(track); + } else { + return sample; + } + } + + // + private int writeEntry64(int offset, long value) throws IOException { + outBackup(); + + auxSeek(offset); + auxWrite(ByteBuffer.allocate(8).putLong(value).array()); + + return offset + 8; + } + + private int writeEntryArray(int offset, int count, int... values) throws IOException { + outBackup(); + + auxSeek(offset); + + int size = count * 4; + ByteBuffer buffer = ByteBuffer.allocate(size); + + for (int i = 0; i < count; i++) { + buffer.putInt(values[i]); + } + + auxWrite(buffer.array()); + + return offset + size; + } + + private void outBackup() { + if (auxBuffer == null && lastWriteOffset < 0) { + lastWriteOffset = writeOffset; + } + } + + /** + * Restore to the previous position before the first call to writeEntry64() + * or writeEntryArray() methods. + */ + private void outRestore() throws IOException { + if (lastWriteOffset > 0) { + outSeek(lastWriteOffset); + lastWriteOffset = -1; + } + } + // + + // + private void outWrite(byte[] buffer) throws IOException { + outWrite(buffer, 0, buffer.length); + } + + private void outWrite(byte[] buffer, int offset, int count) throws IOException { + writeOffset += count; + outStream.write(buffer, offset, count); + } + + private void outSeek(long offset) throws IOException { + if (outStream.canSeek()) { + outStream.seek(offset); + writeOffset = offset; + } else if (outStream.canRewind()) { + outStream.rewind(); + writeOffset = 0; + outSkip(offset); + } else { + throw new IOException("cannot seek or rewind the output stream"); + } + } + + private void outSkip(long amount) throws IOException { + outStream.skip(amount); + writeOffset += amount; + } + + private int lengthFor(int offset) throws IOException { + int size = auxOffset() - offset; + + if (moovSimulation) { + return size; + } + + auxSeek(offset); + auxWrite(size); + auxSkip(size - 4); + + return size; + } + + private int make(int type, int extra, int columns, int rows) throws IOException { + final byte base = 16; + int size = columns * rows * 4; + int total = size + base; + int offset = auxOffset(); + + if (extra >= 0) { + total += 4; + } + + auxWrite(ByteBuffer.allocate(12) + .putInt(total) + .putInt(type) + .putInt(0x00)// default version & flags + .array() + ); + + if (extra >= 0) { + //size += 4;// commented for auxiliar buffer !!! + offset += 4; + auxWrite(extra); + } + + auxWrite(rows); + auxSkip(size); + + return offset + base; + } + + private void auxWrite(int value) throws IOException { + auxWrite(ByteBuffer.allocate(4) + .putInt(value) + .array() + ); + } + + private void auxWrite(byte[] buffer) throws IOException { + if (moovSimulation) { + writeOffset += buffer.length; + } else if (auxBuffer == null) { + outWrite(buffer, 0, buffer.length); + } else { + auxBuffer.put(buffer); + } + } + + private void auxSeek(int offset) throws IOException { + if (moovSimulation) { + writeOffset = offset; + } else if (auxBuffer == null) { + outSeek(offset); + } else { + auxBuffer.position(offset); + } + } + + private void auxSkip(int amount) throws IOException { + if (moovSimulation) { + writeOffset += amount; + } else if (auxBuffer == null) { + outSkip(amount); + } else { + auxBuffer.position(auxBuffer.position() + amount); + } + } + + private int auxOffset() { + return auxBuffer == null ? (int) writeOffset : auxBuffer.position(); + } + // + + // + private int make_ftyp() throws IOException { + byte[] buffer = new byte[]{ + 0x00, 0x00, 0x00, 0x1C, 0x66, 0x74, 0x79, 0x70,// ftyp + 0x6D, 0x70, 0x34, 0x32,// mayor brand (mp42) + 0x00, 0x00, 0x02, 0x00,// default minor version (512) + 0x6D, 0x70, 0x34, 0x31, 0x69, 0x73, 0x6F, 0x6D, 0x69, 0x73, 0x6F, 0x32// compatible brands: mp41 isom iso2 + }; + + if (overrideMainBrand != 0) + ByteBuffer.wrap(buffer).putInt(8, overrideMainBrand); + + outWrite(buffer); + + return buffer.length; + } + + private byte[] make_mdat(long refSize, boolean is64) { + if (is64) { + refSize += 16; + } else { + refSize += 8; + } + + ByteBuffer buffer = ByteBuffer.allocate(is64 ? 16 : 8) + .putInt(is64 ? 0x01 : (int) refSize) + .putInt(0x6D646174);// mdat + + if (is64) { + buffer.putLong(refSize); + } + + return buffer.array(); + } + + private void make_mvhd(long longestTrack) throws IOException { + auxWrite(new byte[]{ + 0x00, 0x00, 0x00, 0x78, 0x6D, 0x76, 0x68, 0x64, 0x01, 0x00, 0x00, 0x00 + }); + auxWrite(ByteBuffer.allocate(28) + .putLong(time) + .putLong(time) + .putInt(DEFAULT_TIMESCALE) + .putLong(longestTrack) + .array() + ); + + auxWrite(new byte[]{ + 0x00, 0x01, 0x00, 0x00, 0x01, 0x00,// default volume and rate + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,// reserved values + // default matrix + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x40, 0x00, 0x00, 0x00 + }); + auxWrite(new byte[24]);// predefined + auxWrite(ByteBuffer.allocate(4) + .putInt(tracks.length + 1) + .array() + ); + } + + private int make_moov(int[] defaultMediaTime, TablesInfo[] tablesInfo, boolean is64) throws RuntimeException, IOException { + int start = auxOffset(); + + auxWrite(new byte[]{ + 0x00, 0x00, 0x00, 0x00, 0x6D, 0x6F, 0x6F, 0x76 + }); + + long longestTrack = 0; + long[] durations = new long[tracks.length]; + + for (int i = 0; i < durations.length; i++) { + durations[i] = (long) Math.ceil( + ((double) tracks[i].trak.tkhd.duration / tracks[i].trak.mdia.mdhd_timeScale) * DEFAULT_TIMESCALE + ); + + if (durations[i] > longestTrack) { + longestTrack = durations[i]; + } + } + + make_mvhd(longestTrack); + + for (int i = 0; i < tracks.length; i++) { + if (tracks[i].trak.tkhd.matrix.length != 36) { + throw new RuntimeException("bad track matrix length (expected 36) in track n°" + i); + } + make_trak(i, durations[i], defaultMediaTime[i], tablesInfo[i], is64); + } + + // udta/meta/ilst/©too + auxWrite(new byte[]{ + 0x00, 0x00, 0x00, 0x5C, 0x75, 0x64, 0x74, 0x61, 0x00, 0x00, 0x00, 0x54, 0x6D, 0x65, 0x74, 0x61, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x68, 0x64, 0x6C, 0x72, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x72, 0x61, 0x70, 0x70, 0x6C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0x69, 0x6C, 0x73, 0x74, 0x00, 0x00, 0x00, + 0x1F, (byte) 0xA9, 0x74, 0x6F, 0x6F, 0x00, 0x00, 0x00, 0x17, 0x64, 0x61, 0x74, 0x61, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string + }); + + return lengthFor(start); + } + + private void make_trak(int index, long duration, int defaultMediaTime, TablesInfo tables, boolean is64) throws IOException { + int start = auxOffset(); + + auxWrite(new byte[]{ + 0x00, 0x00, 0x00, 0x00, 0x74, 0x72, 0x61, 0x6B,// trak header + 0x00, 0x00, 0x00, 0x68, 0x74, 0x6B, 0x68, 0x64, 0x01, 0x00, 0x00, 0x03 // tkhd header + }); + + ByteBuffer buffer = ByteBuffer.allocate(48); + buffer.putLong(time); + buffer.putLong(time); + buffer.putInt(index + 1); + buffer.position(24); + buffer.putLong(duration); + buffer.position(40); + buffer.putShort(tracks[index].trak.tkhd.bLayer); + buffer.putShort(tracks[index].trak.tkhd.bAlternateGroup); + buffer.putShort(tracks[index].trak.tkhd.bVolume); + auxWrite(buffer.array()); + + auxWrite(tracks[index].trak.tkhd.matrix); + auxWrite(ByteBuffer.allocate(8) + .putInt(tracks[index].trak.tkhd.bWidth) + .putInt(tracks[index].trak.tkhd.bHeight) + .array() + ); + + auxWrite(new byte[]{ + 0x00, 0x00, 0x00, 0x24, 0x65, 0x64, 0x74, 0x73,// edts header + 0x00, 0x00, 0x00, 0x1C, 0x65, 0x6C, 0x73, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01// elst header + }); + + int bMediaRate; + int mediaTime; + + if (tracks[index].trak.edst_elst == null) { + // is a audio track ¿is edst/elst opcional for audio tracks? + mediaTime = 0x00;// ffmpeg set this value as zero, instead of defaultMediaTime + bMediaRate = 0x00010000; + } else { + mediaTime = (int) tracks[index].trak.edst_elst.MediaTime; + bMediaRate = tracks[index].trak.edst_elst.bMediaRate; + } + + auxWrite(ByteBuffer + .allocate(12) + .putInt((int) duration) + .putInt(mediaTime) + .putInt(bMediaRate) + .array() + ); + + make_mdia(tracks[index].trak.mdia, tables, is64); + + lengthFor(start); + } + + private void make_mdia(Mdia mdia, TablesInfo tablesInfo, boolean is64) throws IOException { + + int start_mdia = auxOffset(); + auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x6D, 0x64, 0x69, 0x61});// mdia + auxWrite(mdia.mdhd); + auxWrite(make_hdlr(mdia.hdlr)); + + int start_minf = auxOffset(); + auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x6D, 0x69, 0x6E, 0x66});// minf + auxWrite(mdia.minf.$mhd); + auxWrite(mdia.minf.dinf); + + int start_stbl = auxOffset(); + auxWrite(new byte[]{0x00, 0x00, 0x00, 0x00, 0x73, 0x74, 0x62, 0x6C});// stbl + auxWrite(mdia.minf.stbl_stsd); + + // + // In audio tracks the following tables is not required: ssts ctts + // And stsz can be empty if has a default sample size + // + if (moovSimulation) { + make(0x73747473, -1, 2, 1); + if (tablesInfo.stss > 0) { + make(0x73747373, -1, 1, tablesInfo.stss); + } + if (tablesInfo.ctts > 0) { + make(0x63747473, -1, 2, tablesInfo.ctts); + } + make(0x73747363, -1, 3, tablesInfo.stsc); + make(0x7374737A, tablesInfo.stsz_default, 1, tablesInfo.stsz); + make(is64 ? 0x636F3634 : 0x7374636F, -1, is64 ? 2 : 1, tablesInfo.stco); + } else { + tablesInfo.stts = make(0x73747473, -1, 2, 1); + if (tablesInfo.stss > 0) { + tablesInfo.stss = make(0x73747373, -1, 1, tablesInfo.stss); + } + if (tablesInfo.ctts > 0) { + tablesInfo.ctts = make(0x63747473, -1, 2, tablesInfo.ctts); + } + tablesInfo.stsc = make(0x73747363, -1, 3, tablesInfo.stsc); + tablesInfo.stsz = make(0x7374737A, tablesInfo.stsz_default, 1, tablesInfo.stsz); + tablesInfo.stco = make(is64 ? 0x636F3634 : 0x7374636F, -1, is64 ? 2 : 1, tablesInfo.stco); + } + + lengthFor(start_stbl); + lengthFor(start_minf); + lengthFor(start_mdia); + } + + private byte[] make_hdlr(Hdlr hdlr) { + ByteBuffer buffer = ByteBuffer.wrap(new byte[]{ + 0x00, 0x00, 0x00, 0x77, 0x68, 0x64, 0x6C, 0x72,// hdlr + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // binary string "ISO Media file created in NewPipe (A libre lightweight streaming frontend for Android)." + 0x49, 0x53, 0x4F, 0x20, 0x4D, 0x65, 0x64, 0x69, 0x61, 0x20, 0x66, 0x69, 0x6C, 0x65, 0x20, 0x63, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x20, 0x69, 0x6E, 0x20, 0x4E, 0x65, 0x77, 0x50, 0x69, 0x70, + 0x65, 0x20, 0x28, 0x41, 0x20, 0x6C, 0x69, 0x62, 0x72, 0x65, 0x20, 0x6C, 0x69, 0x67, 0x68, 0x74, + 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x20, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6D, 0x69, 0x6E, 0x67, + 0x20, 0x66, 0x72, 0x6F, 0x6E, 0x74, 0x65, 0x6E, 0x64, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x41, 0x6E, + 0x64, 0x72, 0x6F, 0x69, 0x64, 0x29, 0x2E + }); + + buffer.position(12); + buffer.putInt(hdlr.type); + buffer.putInt(hdlr.subType); + buffer.put(hdlr.bReserved);// always is a zero array + + return buffer.array(); + } + // + + class TablesInfo { + + public int stts; + public int stsc; + public int[] stsc_bEntries; + public int ctts; + public int stsz; + public int stsz_default; + public int stss; + public int stco; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/streams/SubtitleConverter.java b/app/src/main/java/org/schabi/newpipe/streams/SubtitleConverter.java index 26aaf49a5..c41db4373 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/SubtitleConverter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/SubtitleConverter.java @@ -1,5 +1,6 @@ package org.schabi.newpipe.streams; +import org.schabi.newpipe.streams.io.SharpStream; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; @@ -12,8 +13,6 @@ import java.nio.charset.Charset; import java.text.ParseException; import java.util.Locale; -import org.schabi.newpipe.streams.io.SharpStream; - import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; @@ -27,11 +26,11 @@ public class SubtitleConverter { public void dumpTTML(SharpStream in, final SharpStream out, final boolean ignoreEmptyFrames, final boolean detectYoutubeDuplicateLines ) throws IOException, ParseException, SAXException, ParserConfigurationException, XPathExpressionException { - + final FrameWriter callback = new FrameWriter() { int frameIndex = 0; final Charset charset = Charset.forName("utf-8"); - + @Override public void yield(SubtitleFrame frame) throws IOException { if (ignoreEmptyFrames && frame.isEmptyText()) { @@ -48,13 +47,13 @@ public class SubtitleConverter { out.write(NEW_LINE.getBytes(charset)); } }; - + read_xml_based(in, callback, detectYoutubeDuplicateLines, "tt", "xmlns", "http://www.w3.org/ns/ttml", new String[]{"timedtext", "head", "wp"}, new String[]{"body", "div", "p"}, "begin", "end", true - ); + ); } private void read_xml_based(SharpStream source, FrameWriter callback, boolean detectYoutubeDuplicateLines, @@ -70,7 +69,7 @@ public class SubtitleConverter { * Language parsing is not supported */ - byte[] buffer = new byte[source.available()]; + byte[] buffer = new byte[(int) source.available()]; source.read(buffer); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); @@ -206,7 +205,7 @@ public class SubtitleConverter { } } - private static NodeList selectNodes(Document xml, String[] path, String namespaceUri) throws XPathExpressionException { + private static NodeList selectNodes(Document xml, String[] path, String namespaceUri) { Element ref = xml.getDocumentElement(); for (int i = 0; i < path.length - 1; i++) { diff --git a/app/src/main/java/org/schabi/newpipe/streams/TrackDataChunk.java b/app/src/main/java/org/schabi/newpipe/streams/TrackDataChunk.java deleted file mode 100644 index 86eb5ff4f..000000000 --- a/app/src/main/java/org/schabi/newpipe/streams/TrackDataChunk.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.schabi.newpipe.streams; - -import java.io.InputStream; -import java.io.IOException; - -public class TrackDataChunk extends InputStream { - - private final DataReader base; - private int size; - - public TrackDataChunk(DataReader base, int size) { - this.base = base; - this.size = size; - } - - @Override - public int read() throws IOException { - if (size < 1) { - return -1; - } - - int res = base.read(); - - if (res >= 0) { - size--; - } - - return res; - } - - @Override - public int read(byte[] buffer) throws IOException { - return read(buffer, 0, buffer.length); - } - - @Override - public int read(byte[] buffer, int offset, int count) throws IOException { - count = Math.min(size, count); - int read = base.read(buffer, offset, count); - size -= count; - return read; - } - - @Override - public long skip(long amount) throws IOException { - long res = base.skipBytes(Math.min(amount, size)); - size -= res; - return res; - } - - @Override - public int available() { - return size; - } - - @Override - public void close() { - size = 0; - } - - @Override - public boolean markSupported() { - return false; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/streams/WebMReader.java b/app/src/main/java/org/schabi/newpipe/streams/WebMReader.java index f61ef14c5..0c635ebe3 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/WebMReader.java +++ b/app/src/main/java/org/schabi/newpipe/streams/WebMReader.java @@ -1,12 +1,13 @@ package org.schabi.newpipe.streams; +import org.schabi.newpipe.streams.io.SharpStream; + import java.io.EOFException; import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.NoSuchElementException; -import java.util.Objects; - -import org.schabi.newpipe.streams.io.SharpStream; /** * @@ -121,7 +122,7 @@ public class WebMReader { } private String readString(Element parent) throws IOException { - return new String(readBlob(parent), "utf-8"); + return new String(readBlob(parent), StandardCharsets.UTF_8);// or use "utf-8" } private byte[] readBlob(Element parent) throws IOException { @@ -193,6 +194,7 @@ public class WebMReader { return elem; } } + ensure(elem); } @@ -306,7 +308,7 @@ public class WebMReader { entry.trackNumber = readNumber(elem); break; case ID_TrackType: - entry.trackType = (int)readNumber(elem); + entry.trackType = (int) readNumber(elem); break; case ID_CodecID: entry.codecId = readString(elem); @@ -445,7 +447,7 @@ public class WebMReader { public class SimpleBlock { - public TrackDataChunk data; + public InputStream data; SimpleBlock(Element ref) { this.ref = ref; @@ -492,7 +494,7 @@ public class WebMReader { currentSimpleBlock = readSimpleBlock(elem); if (currentSimpleBlock.trackNumber == tracks[selectedTrack].trackNumber) { - currentSimpleBlock.data = new TrackDataChunk(stream, (int) currentSimpleBlock.dataSize); + currentSimpleBlock.data = stream.getView((int) currentSimpleBlock.dataSize); return currentSimpleBlock; } diff --git a/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java b/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java index ea038c607..eba2bbb87 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java @@ -1,20 +1,20 @@ package org.schabi.newpipe.streams; +import android.support.annotation.NonNull; + import org.schabi.newpipe.streams.WebMReader.Cluster; import org.schabi.newpipe.streams.WebMReader.Segment; import org.schabi.newpipe.streams.WebMReader.SimpleBlock; import org.schabi.newpipe.streams.WebMReader.WebMTrack; +import org.schabi.newpipe.streams.io.SharpStream; import java.io.IOException; import java.io.InputStream; -import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import org.schabi.newpipe.streams.io.SharpStream; - /** - * * @author kapodamy */ public class WebMWriter { @@ -94,10 +94,6 @@ public class WebMWriter { } } - public long getBytesWritten() { - return written; - } - public boolean isDone() { return done; } @@ -138,42 +134,42 @@ public class WebMWriter { /* segment */ listBuffer.add(new byte[]{ - 0x18, 0x53, (byte) 0x80, 0x67, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00// segment content size + 0x18, 0x53, (byte) 0x80, 0x67, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00// segment content size }); long baseSegmentOffset = written + listBuffer.get(0).length; /* seek head */ listBuffer.add(new byte[]{ - 0x11, 0x4d, (byte) 0x9b, 0x74, (byte) 0xbe, - 0x4d, (byte) 0xbb, (byte) 0x8b, - 0x53, (byte) 0xab, (byte) 0x84, 0x15, 0x49, (byte) 0xa9, 0x66, 0x53, - (byte) 0xac, (byte) 0x81, /*info offset*/ 0x43, - 0x4d, (byte) 0xbb, (byte) 0x8b, 0x53, (byte) 0xab, - (byte) 0x84, 0x16, 0x54, (byte) 0xae, 0x6b, 0x53, (byte) 0xac, (byte) 0x81, - /*tracks offset*/ 0x6a, - 0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1f, - 0x43, (byte) 0xb6, 0x75, 0x53, (byte) 0xac, (byte) 0x84, /*cluster offset [2]*/ 0x00, 0x00, 0x00, 0x00, - 0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1c, 0x53, - (byte) 0xbb, 0x6b, 0x53, (byte) 0xac, (byte) 0x84, /*cues offset [7]*/ 0x00, 0x00, 0x00, 0x00 + 0x11, 0x4d, (byte) 0x9b, 0x74, (byte) 0xbe, + 0x4d, (byte) 0xbb, (byte) 0x8b, + 0x53, (byte) 0xab, (byte) 0x84, 0x15, 0x49, (byte) 0xa9, 0x66, 0x53, + (byte) 0xac, (byte) 0x81, /*info offset*/ 0x43, + 0x4d, (byte) 0xbb, (byte) 0x8b, 0x53, (byte) 0xab, + (byte) 0x84, 0x16, 0x54, (byte) 0xae, 0x6b, 0x53, (byte) 0xac, (byte) 0x81, + /*tracks offset*/ 0x6a, + 0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1f, + 0x43, (byte) 0xb6, 0x75, 0x53, (byte) 0xac, (byte) 0x84, /*cluster offset [2]*/ 0x00, 0x00, 0x00, 0x00, + 0x4d, (byte) 0xbb, (byte) 0x8e, 0x53, (byte) 0xab, (byte) 0x84, 0x1c, 0x53, + (byte) 0xbb, 0x6b, 0x53, (byte) 0xac, (byte) 0x84, /*cues offset [7]*/ 0x00, 0x00, 0x00, 0x00 }); /* info */ listBuffer.add(new byte[]{ - 0x15, 0x49, (byte) 0xa9, 0x66, (byte) 0xa2, 0x2a, (byte) 0xd7, (byte) 0xb1 + 0x15, 0x49, (byte) 0xa9, 0x66, (byte) 0xa2, 0x2a, (byte) 0xd7, (byte) 0xb1 }); listBuffer.add(encode(DEFAULT_TIMECODE_SCALE, true));// this value MUST NOT exceed 4 bytes listBuffer.add(new byte[]{0x44, (byte) 0x89, (byte) 0x84, - 0x00, 0x00, 0x00, 0x00,// info.duration - - /* MuxingApp */ - 0x4d, (byte) 0x80, (byte) 0x87, 0x4E, - 0x65, 0x77, 0x50, 0x69, 0x70, 0x65, // "NewPipe" binary string - - /* WritingApp */ - 0x57, 0x41, (byte) 0x87, 0x4E, - 0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string + 0x00, 0x00, 0x00, 0x00,// info.duration + + /* MuxingApp */ + 0x4d, (byte) 0x80, (byte) 0x87, 0x4E, + 0x65, 0x77, 0x50, 0x69, 0x70, 0x65, // "NewPipe" binary string + + /* WritingApp */ + 0x57, 0x41, (byte) 0x87, 0x4E, + 0x65, 0x77, 0x50, 0x69, 0x70, 0x65// "NewPipe" binary string }); /* tracks */ @@ -200,7 +196,6 @@ public class WebMWriter { long nextCueTime = infoTracks[cuesForTrackId].trackType == 1 ? -1 : 0; ArrayList keyFrames = new ArrayList<>(32); - //ArrayList chunks = new ArrayList<>(readers.length); ArrayList clusterOffsets = new ArrayList<>(32); ArrayList clusterSizes = new ArrayList<>(32); @@ -283,24 +278,21 @@ public class WebMWriter { long segmentSize = written - offsetSegmentSizeSet - 7; - // final step write offsets and sizes - out.rewind(); - written = 0; - - skipTo(out, offsetSegmentSizeSet); + /* ---- final step write offsets and sizes ---- */ + seekTo(out, offsetSegmentSizeSet); writeLong(out, segmentSize); if (predefinedDurations[durationFromTrackId] > -1) { duration += predefinedDurations[durationFromTrackId];// this value is full-filled in makeTrackEntry() method } - skipTo(out, offsetInfoDurationSet); + seekTo(out, offsetInfoDurationSet); writeFloat(out, duration); firstClusterOffset -= baseSegmentOffset; - skipTo(out, offsetClusterSet); + seekTo(out, offsetClusterSet); writeInt(out, firstClusterOffset); - skipTo(out, cueReservedOffset); + seekTo(out, cueReservedOffset); /* Cue */ dump(new byte[]{0x1c, 0x53, (byte) 0xbb, 0x6b, 0x20, 0x00, 0x00}, out); @@ -321,17 +313,14 @@ public class WebMWriter { voidBuffer.putShort((short) (firstClusterOffset - written - 4)); dump(voidBuffer.array(), out); - out.rewind(); - written = 0; - - skipTo(out, offsetCuesSet); + seekTo(out, offsetCuesSet); writeInt(out, (int) (cueReservedOffset - baseSegmentOffset)); - skipTo(out, cueReservedOffset + 5); + seekTo(out, cueReservedOffset + 5); writeShort(out, cueSize); for (int i = 0; i < clusterSizes.size(); i++) { - skipTo(out, clusterOffsets.get(i)); + seekTo(out, clusterOffsets.get(i)); byte[] size = ByteBuffer.allocate(4).putInt(clusterSizes.get(i) | 0x200000).array(); out.write(size, 1, 3); written += 3; @@ -365,20 +354,29 @@ public class WebMWriter { bloq.dataSize = (int) res.dataSize; bloq.trackNumber = internalTrackId; bloq.flags = res.flags; - bloq.absoluteTimecode = convertTimecode(res.relativeTimeCode, readersSegment[internalTrackId].info.timecodeScale, DEFAULT_TIMECODE_SCALE); + bloq.absoluteTimecode = convertTimecode(res.relativeTimeCode, readersSegment[internalTrackId].info.timecodeScale); bloq.absoluteTimecode += readersCluter[internalTrackId].timecode; return bloq; } - private short convertTimecode(int time, long oldTimeScale, int newTimeScale) { - return (short) (time * (newTimeScale / oldTimeScale)); + private short convertTimecode(int time, long oldTimeScale) { + return (short) (time * (DEFAULT_TIMECODE_SCALE / oldTimeScale)); } - private void skipTo(SharpStream stream, long absoluteOffset) throws IOException { - absoluteOffset -= written; - written += absoluteOffset; - stream.skip(absoluteOffset); + private void seekTo(SharpStream stream, long offset) throws IOException { + if (stream.canSeek()) { + stream.seek(offset); + } else { + if (offset > written) { + stream.skip(offset - written); + } else { + stream.rewind(); + stream.skip(offset); + } + } + + written = offset; } private void writeLong(SharpStream stream, long number) throws IOException { @@ -468,12 +466,12 @@ public class WebMWriter { private void makeEBML(SharpStream stream) throws IOException { // deafult values dump(new byte[]{ - 0x1A, 0x45, (byte) 0xDF, (byte) 0xA3, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x1F, 0x42, (byte) 0x86, (byte) 0x81, 0x01, - 0x42, (byte) 0xF7, (byte) 0x81, 0x01, 0x42, (byte) 0xF2, (byte) 0x81, 0x04, - 0x42, (byte) 0xF3, (byte) 0x81, 0x08, 0x42, (byte) 0x82, (byte) 0x84, 0x77, - 0x65, 0x62, 0x6D, 0x42, (byte) 0x87, (byte) 0x81, 0x02, - 0x42, (byte) 0x85, (byte) 0x81, 0x02 + 0x1A, 0x45, (byte) 0xDF, (byte) 0xA3, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x1F, 0x42, (byte) 0x86, (byte) 0x81, 0x01, + 0x42, (byte) 0xF7, (byte) 0x81, 0x01, 0x42, (byte) 0xF2, (byte) 0x81, 0x04, + 0x42, (byte) 0xF3, (byte) 0x81, 0x08, 0x42, (byte) 0x82, (byte) 0x84, 0x77, + 0x65, 0x62, 0x6D, 0x42, (byte) 0x87, (byte) 0x81, 0x02, + 0x42, (byte) 0x85, (byte) 0x81, 0x02 }, stream); } @@ -618,9 +616,10 @@ public class WebMWriter { int offset = withLength ? 1 : 0; byte[] buffer = new byte[offset + length]; - long marker = (long) Math.floor((length - 1) / 8); + long marker = (long) Math.floor((length - 1f) / 8f); - for (int i = length - 1, mul = 1; i >= 0; i--, mul *= 0x100) { + float mul = 1; + for (int i = length - 1; i >= 0; i--, mul *= 0x100) { long b = (long) Math.floor(number / mul); if (!withLength && i == marker) { b = b | (0x80 >> (length - 1)); @@ -637,11 +636,7 @@ public class WebMWriter { private ArrayList encode(String value) { byte[] str; - try { - str = value.getBytes("utf-8"); - } catch (UnsupportedEncodingException err) { - str = value.getBytes(); - } + str = value.getBytes(StandardCharsets.UTF_8);// or use "utf-8" ArrayList buffer = new ArrayList<>(2); buffer.add(encode(str.length, false)); @@ -720,9 +715,10 @@ public class WebMWriter { return (flags & 0x80) == 0x80; } + @NonNull @Override public String toString() { - return String.format("trackNumber=%s isKeyFrame=%S absoluteTimecode=%s", trackNumber, (flags & 0x80) == 0x80, absoluteTimecode); + return String.format("trackNumber=%s isKeyFrame=%S absoluteTimecode=%s", trackNumber, isKeyframe(), absoluteTimecode); } } } diff --git a/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java b/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java index 48bea06f6..ea2f60837 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java +++ b/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java @@ -3,7 +3,7 @@ package org.schabi.newpipe.streams.io; import java.io.IOException; /** - * based c# + * based on c# */ public abstract class SharpStream { @@ -15,23 +15,27 @@ public abstract class SharpStream { public abstract long skip(long amount) throws IOException; - - public abstract int available(); + public abstract long available(); public abstract void rewind() throws IOException; - public abstract void dispose(); public abstract boolean isDisposed(); - public abstract boolean canRewind(); public abstract boolean canRead(); public abstract boolean canWrite(); + public boolean canSetLength() { + return false; + } + + public boolean canSeek() { + return false; + } public abstract void write(byte value) throws IOException; @@ -39,9 +43,15 @@ public abstract class SharpStream { public abstract void write(byte[] buffer, int offset, int count) throws IOException; - public abstract void flush() throws IOException; + public void flush() throws IOException { + // STUB + } public void setLength(long length) throws IOException { throw new IOException("Not implemented"); } + + public void seek(long offset) throws IOException { + throw new IOException("Not implemented"); + } } diff --git a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java index 8fc423837..fa5530f12 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java @@ -430,24 +430,26 @@ public final class ListHelper { */ private static String getResolutionLimit(Context context) { String resolutionLimit = null; - if (!isWifiActive(context)) { + if (isMeteredNetwork(context)) { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); String defValue = context.getString(R.string.limit_data_usage_none_key); String value = preferences.getString( context.getString(R.string.limit_mobile_data_usage_key), defValue); - resolutionLimit = value.equals(defValue) ? null : value; + resolutionLimit = defValue.equals(value) ? null : value; } return resolutionLimit; } /** - * Are we connected to wifi? + * The current network is metered (like mobile data)? * @param context App context - * @return {@code true} if connected to wifi + * @return {@code true} if connected to a metered network */ - private static boolean isWifiActive(Context context) + private static boolean isMeteredNetwork(Context context) { ConnectivityManager manager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); - return manager != null && manager.getActiveNetworkInfo() != null && manager.getActiveNetworkInfo().getType() == ConnectivityManager.TYPE_WIFI; + if (manager == null || manager.getActiveNetworkInfo() == null) return false; + + return manager.isActiveNetworkMetered(); } } diff --git a/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java b/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java index b3522aea0..7febfa053 100644 --- a/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java @@ -38,7 +38,7 @@ public class SecondaryStreamHelper { public static AudioStream getAudioStreamFor(@NonNull List audioStreams, @NonNull VideoStream videoStream) { switch (videoStream.getFormat()) { case WEBM: - case MPEG_4: + case MPEG_4:// ¿is mpeg-4 DASH? break; default: return null; diff --git a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java index b864cf4fb..abc934878 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java @@ -164,9 +164,6 @@ public class DownloadInitializer extends Thread { } } - // hide marquee in the progress bar - mMission.done++; - mMission.start(); } diff --git a/app/src/main/java/us/shandian/giga/get/DownloadMission.java b/app/src/main/java/us/shandian/giga/get/DownloadMission.java index 243a8585a..b8849482a 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMission.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMission.java @@ -40,6 +40,9 @@ public class DownloadMission extends Mission { public static final int ERROR_UNKNOWN_HOST = 1005; public static final int ERROR_CONNECT_HOST = 1006; public static final int ERROR_POSTPROCESSING = 1007; + public static final int ERROR_POSTPROCESSING_STOPPED = 1008; + public static final int ERROR_POSTPROCESSING_HOLD = 1009; + public static final int ERROR_INSUFFICIENT_STORAGE = 1010; public static final int ERROR_HTTP_NO_CONTENT = 204; public static final int ERROR_HTTP_UNSUPPORTED_RANGE = 206; @@ -83,8 +86,9 @@ public class DownloadMission extends Mission { * 0: ready * 1: running * 2: completed + * 3: hold */ - public int postprocessingState; + public volatile int postprocessingState; /** * Indicate if the post-processing algorithm works on the same file @@ -92,19 +96,19 @@ public class DownloadMission extends Mission { public boolean postprocessingThis; /** - * The current resource to download {@code urls[current]} + * The current resource to download, see {@code urls[current]} and {@code offsets[current]} */ public int current; /** * Metadata where the mission state is saved */ - public File metadata; + public transient File metadata; /** * maximum attempts */ - public int maxRetry; + public transient int maxRetry; /** * Approximated final length, this represent the sum of all resources sizes @@ -115,11 +119,11 @@ public class DownloadMission extends Mission { boolean fallback; private int finishCount; public transient boolean running; - public transient boolean enqueued = true; + public boolean enqueued; public int errCode = ERROR_NOTHING; - public transient Exception errObject = null; + public Exception errObject = null; public transient boolean recovered; public transient Handler mHandler; private transient boolean mWritingToFile; @@ -131,7 +135,7 @@ public class DownloadMission extends Mission { private transient boolean deleted; int currentThreadCount; - private transient Thread[] threads = new Thread[0]; + public transient volatile Thread[] threads = new Thread[0]; private transient Thread init = null; @@ -155,6 +159,8 @@ public class DownloadMission extends Mission { this.location = location; this.kind = kind; this.offsets = new long[urls.length]; + this.enqueued = true; + this.maxRetry = 3; if (postprocessingName != null) { Postprocessing algorithm = Postprocessing.getAlgorithm(postprocessingName, null); @@ -183,6 +189,7 @@ public class DownloadMission extends Mission { */ boolean isBlockPreserved(long block) { checkBlock(block); + //noinspection ConstantConditions return blockState.containsKey(block) ? blockState.get(block) : false; } @@ -247,6 +254,12 @@ public class DownloadMission extends Mission { HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setInstanceFollowRedirects(true); + // BUG workaround: switching between networks can freeze the download forever + + //conn.setRequestProperty("Connection", "close"); + conn.setConnectTimeout(30000); + conn.setReadTimeout(10000); + if (rangeStart >= 0) { String req = "bytes=" + rangeStart + "-"; if (rangeEnd > 0) req += rangeEnd; @@ -342,11 +355,32 @@ public class DownloadMission extends Mission { } } - synchronized void notifyError(int code, Exception err) { + public synchronized void notifyError(int code, Exception err) { Log.e(TAG, "notifyError() code = " + code, err); + if (err instanceof IOException) { + if (err.getMessage().contains("Permission denied")) { + code = ERROR_PERMISSION_DENIED; + err = null; + } else if (err.getMessage().contains("write failed: ENOSPC")) { + code = ERROR_INSUFFICIENT_STORAGE; + err = null; + } else { + try { + File storage = new File(location); + if (storage.canWrite() && storage.getUsableSpace() < (getLength() - done)) { + code = ERROR_INSUFFICIENT_STORAGE; + err = null; + } + } catch (SecurityException e) { + // is a permission error + } + } + } + errCode = code; errObject = err; + enqueued = false; pause(); @@ -378,6 +412,7 @@ public class DownloadMission extends Mission { if (!doPostprocessing()) return; + enqueued = false; running = false; deleteThisFromFile(); @@ -386,22 +421,20 @@ public class DownloadMission extends Mission { } private void notifyPostProcessing(int state) { - if (DEBUG) { - String action; - switch (state) { - case 1: - action = "Running"; - break; - case 2: - action = "Completed"; - break; - default: - action = "Failed"; - } - - Log.d(TAG, action + " postprocessing on " + location + File.separator + name); + String action; + switch (state) { + case 1: + action = "Running"; + break; + case 2: + action = "Completed"; + break; + default: + action = "Failed"; } + Log.d(TAG, action + " postprocessing on " + location + File.separator + name); + synchronized (blockState) { // don't return without fully write the current state postprocessingState = state; @@ -420,7 +453,6 @@ public class DownloadMission extends Mission { if (threads != null) for (Thread thread : threads) joinForThread(thread); - enqueued = false; running = true; errCode = ERROR_NOTHING; @@ -463,7 +495,7 @@ public class DownloadMission extends Mission { } /** - * Pause the mission, does not affect the blocks that are being downloaded. + * Pause the mission */ public synchronized void pause() { if (!running) return; @@ -477,7 +509,6 @@ public class DownloadMission extends Mission { running = false; recovered = true; - enqueued = false; if (init != null && Thread.currentThread() != init && init.isAlive()) { init.interrupt(); @@ -514,7 +545,7 @@ public class DownloadMission extends Mission { } /** - * Removes the file and the meta file + * Removes the downloaded file and the meta file */ @Override public boolean delete() { @@ -580,12 +611,21 @@ public class DownloadMission extends Mission { * @return true, otherwise, false */ public boolean isPsRunning() { - return postprocessingName != null && postprocessingState == 1; + return postprocessingName != null && (postprocessingState == 1 || postprocessingState == 3); + } + + /** + * Indicated if the mission is ready + * + * @return true, otherwise, false + */ + public boolean isInitialized() { + return blocks >= 0; // DownloadMissionInitializer was executed } public long getLength() { long calculated; - if (postprocessingState == 1) { + if (postprocessingState == 1 || postprocessingState == 3) { calculated = length; } else { calculated = offsets[current < offsets.length ? current : (offsets.length - 1)] + length; @@ -596,13 +636,37 @@ public class DownloadMission extends Mission { return calculated > nearLength ? calculated : nearLength; } + /** + * set this mission state on the queue + * + * @param queue true to add to the queue, otherwise, false + */ + public void setEnqueued(boolean queue) { + enqueued = queue; + runAsync(-2, this::writeThisToFile); + } + + /** + * Attempts to continue a blocked post-processing + * + * @param recover {@code true} to retry, otherwise, {@code false} to cancel + */ + public void psContinue(boolean recover) { + postprocessingState = 1; + errCode = recover ? ERROR_NOTHING : ERROR_POSTPROCESSING; + threads[0].interrupt(); + } + private boolean doPostprocessing() { if (postprocessingName == null || postprocessingState == 2) return true; notifyPostProcessing(1); notifyProgress(0); - Thread.currentThread().setName("[" + TAG + "] post-processing = " + postprocessingName + " filename = " + name); + if (DEBUG) + Thread.currentThread().setName("[" + TAG + "] post-processing = " + postprocessingName + " filename = " + name); + + threads = new Thread[]{Thread.currentThread()}; Exception exception = null; diff --git a/app/src/main/java/us/shandian/giga/get/Mission.java b/app/src/main/java/us/shandian/giga/get/Mission.java index ec2ddaa26..53c81b08b 100644 --- a/app/src/main/java/us/shandian/giga/get/Mission.java +++ b/app/src/main/java/us/shandian/giga/get/Mission.java @@ -47,6 +47,11 @@ public abstract class Mission implements Serializable { return new File(location, name); } + /** + * Delete the downloaded file + * + * @return {@code true] if and only if the file is successfully deleted, otherwise, {@code false} + */ public boolean delete() { deleted = true; return getDownloadedFile().delete(); diff --git a/app/src/main/java/us/shandian/giga/postprocessing/M4aNoDash.java b/app/src/main/java/us/shandian/giga/postprocessing/M4aNoDash.java new file mode 100644 index 000000000..fa0c2c7ae --- /dev/null +++ b/app/src/main/java/us/shandian/giga/postprocessing/M4aNoDash.java @@ -0,0 +1,43 @@ +package us.shandian.giga.postprocessing; + +import org.schabi.newpipe.streams.Mp4DashReader; +import org.schabi.newpipe.streams.Mp4FromDashWriter; +import org.schabi.newpipe.streams.io.SharpStream; + +import java.io.IOException; + +import us.shandian.giga.get.DownloadMission; + +public class M4aNoDash extends Postprocessing { + + M4aNoDash(DownloadMission mission) { + super(mission, 0, true); + } + + @Override + boolean test(SharpStream... sources) throws IOException { + // check if the mp4 file is DASH (youtube) + + Mp4DashReader reader = new Mp4DashReader(sources[0]); + reader.parse(); + + switch (reader.getBrands()[0]) { + case 0x64617368:// DASH + case 0x69736F35:// ISO5 + return true; + default: + return false; + } + } + + @Override + int process(SharpStream out, SharpStream... sources) throws IOException { + Mp4FromDashWriter muxer = new Mp4FromDashWriter(sources[0]); + muxer.setMainBrand(0x4D344120);// binary string "M4A " + muxer.parseSources(); + muxer.selectTracks(0); + muxer.build(out); + + return OK_RESULT; + } +} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Mp4DashMuxer.java b/app/src/main/java/us/shandian/giga/postprocessing/Mp4FromDashMuxer.java similarity index 60% rename from app/src/main/java/us/shandian/giga/postprocessing/Mp4DashMuxer.java rename to app/src/main/java/us/shandian/giga/postprocessing/Mp4FromDashMuxer.java index 45c06dd4b..09f5d9661 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/Mp4DashMuxer.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/Mp4FromDashMuxer.java @@ -1,29 +1,29 @@ -package us.shandian.giga.postprocessing; - -import org.schabi.newpipe.streams.Mp4DashWriter; -import org.schabi.newpipe.streams.io.SharpStream; - -import java.io.IOException; - -import us.shandian.giga.get.DownloadMission; - -/** - * @author kapodamy - */ -class Mp4DashMuxer extends Postprocessing { - - Mp4DashMuxer(DownloadMission mission) { - super(mission, 15360 * 1024/* 15 MiB */, true); - } - - @Override - int process(SharpStream out, SharpStream... sources) throws IOException { - Mp4DashWriter muxer = new Mp4DashWriter(sources); - muxer.parseSources(); - muxer.selectTracks(0, 0); - muxer.build(out); - - return OK_RESULT; - } - -} +package us.shandian.giga.postprocessing; + +import org.schabi.newpipe.streams.Mp4FromDashWriter; +import org.schabi.newpipe.streams.io.SharpStream; + +import java.io.IOException; + +import us.shandian.giga.get.DownloadMission; + +/** + * @author kapodamy + */ +class Mp4FromDashMuxer extends Postprocessing { + + Mp4FromDashMuxer(DownloadMission mission) { + super(mission, 2 * 1024 * 1024/* 2 MiB */, true); + } + + @Override + int process(SharpStream out, SharpStream... sources) throws IOException { + Mp4FromDashWriter muxer = new Mp4FromDashWriter(sources); + muxer.parseSources(); + muxer.selectTracks(0, 0); + muxer.build(out); + + return OK_RESULT; + } + +} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Mp4Muxer.java b/app/src/main/java/us/shandian/giga/postprocessing/Mp4Muxer.java deleted file mode 100644 index bf932d5c1..000000000 --- a/app/src/main/java/us/shandian/giga/postprocessing/Mp4Muxer.java +++ /dev/null @@ -1,136 +0,0 @@ -package us.shandian.giga.postprocessing; - -import android.media.MediaCodec.BufferInfo; -import android.media.MediaExtractor; -import android.media.MediaMuxer; -import android.media.MediaMuxer.OutputFormat; -import android.util.Log; - -import static org.schabi.newpipe.BuildConfig.DEBUG; - -import org.schabi.newpipe.streams.io.SharpStream; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.nio.ByteBuffer; - -import us.shandian.giga.get.DownloadMission; - - -class Mp4Muxer extends Postprocessing { - private static final String TAG = "Mp4Muxer"; - private static final int NOTIFY_BYTES_INTERVAL = 128 * 1024;// 128 KiB - - Mp4Muxer(DownloadMission mission) { - super(mission, 0, false); - } - - @Override - int process(SharpStream out, SharpStream... sources) throws IOException { - File dlFile = mission.getDownloadedFile(); - File tmpFile = new File(mission.location, mission.name.concat(".tmp")); - - if (tmpFile.exists()) - if (!tmpFile.delete()) return DownloadMission.ERROR_FILE_CREATION; - - if (!tmpFile.createNewFile()) return DownloadMission.ERROR_FILE_CREATION; - - FileInputStream source = null; - MediaMuxer muxer = null; - - //noinspection TryFinallyCanBeTryWithResources - try { - source = new FileInputStream(dlFile); - MediaExtractor tracks[] = { - getMediaExtractor(source, mission.offsets[0], mission.offsets[1] - mission.offsets[0]), - getMediaExtractor(source, mission.offsets[1], mission.length - mission.offsets[1]) - }; - - muxer = new MediaMuxer(tmpFile.getAbsolutePath(), OutputFormat.MUXER_OUTPUT_MPEG_4); - - int tracksIndex[] = { - muxer.addTrack(tracks[0].getTrackFormat(0)), - muxer.addTrack(tracks[1].getTrackFormat(0)) - }; - - ByteBuffer buffer = ByteBuffer.allocate(512 * 1024);// 512 KiB - BufferInfo info = new BufferInfo(); - - long written = 0; - long nextReport = NOTIFY_BYTES_INTERVAL; - - muxer.start(); - - while (true) { - int done = 0; - - for (int i = 0; i < tracks.length; i++) { - if (tracksIndex[i] < 0) continue; - - info.set(0, - tracks[i].readSampleData(buffer, 0), - tracks[i].getSampleTime(), - tracks[i].getSampleFlags() - ); - - if (info.size >= 0) { - muxer.writeSampleData(tracksIndex[i], buffer, info); - written += info.size; - done++; - } - if (!tracks[i].advance()) { - // EOF reached - tracks[i].release(); - tracksIndex[i] = -1; - } - - if (written > nextReport) { - nextReport = written + NOTIFY_BYTES_INTERVAL; - super.progressReport(written); - } - } - - if (done < 1) break; - } - - // this part should not fail - if (!dlFile.delete()) return DownloadMission.ERROR_FILE_CREATION; - if (!tmpFile.renameTo(dlFile)) return DownloadMission.ERROR_FILE_CREATION; - - return OK_RESULT; - } finally { - try { - if (muxer != null) { - muxer.stop(); - muxer.release(); - } - } catch (Exception err) { - if (DEBUG) - Log.e(TAG, "muxer stop/release failed", err); - } - - if (source != null) { - try { - source.close(); - } catch (IOException e) { - // nothing to do - } - } - - // if the operation fails, delete the temporal file - if (tmpFile.exists()) { - //noinspection ResultOfMethodCallIgnored - tmpFile.delete(); - } - } - } - - private MediaExtractor getMediaExtractor(FileInputStream source, long offset, long length) throws IOException { - MediaExtractor extractor = new MediaExtractor(); - extractor.setDataSource(source.getFD(), offset, length); - extractor.selectTrack(0); - - return extractor; - } -} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java index 635140bd3..df8549010 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java @@ -1,6 +1,7 @@ package us.shandian.giga.postprocessing; import android.os.Message; +import android.util.Log; import org.schabi.newpipe.streams.io.SharpStream; @@ -9,17 +10,22 @@ import java.io.IOException; import us.shandian.giga.get.DownloadMission; import us.shandian.giga.postprocessing.io.ChunkFileInputStream; -import us.shandian.giga.postprocessing.io.CircularFile; +import us.shandian.giga.postprocessing.io.CircularFileWriter; +import us.shandian.giga.postprocessing.io.CircularFileWriter.OffsetChecker; import us.shandian.giga.service.DownloadManagerService; +import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING; +import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD; +import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION; + public abstract class Postprocessing { - static final byte OK_RESULT = DownloadMission.ERROR_NOTHING; + static final byte OK_RESULT = ERROR_NOTHING; public static final String ALGORITHM_TTML_CONVERTER = "ttml"; - public static final String ALGORITHM_MP4_DASH_MUXER = "mp4D"; - public static final String ALGORITHM_MP4_MUXER = "mp4"; public static final String ALGORITHM_WEBM_MUXER = "webm"; + public static final String ALGORITHM_MP4_FROM_DASH_MUXER = "mp4D-mp4"; + public static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a"; public static Postprocessing getAlgorithm(String algorithmName, DownloadMission mission) { if (null == algorithmName) { @@ -27,14 +33,14 @@ public abstract class Postprocessing { } else switch (algorithmName) { case ALGORITHM_TTML_CONVERTER: return new TtmlConverter(mission); - case ALGORITHM_MP4_DASH_MUXER: - return new Mp4DashMuxer(mission); - case ALGORITHM_MP4_MUXER: - return new Mp4Muxer(mission); case ALGORITHM_WEBM_MUXER: return new WebMMuxer(mission); + case ALGORITHM_MP4_FROM_DASH_MUXER: + return new Mp4FromDashMuxer(mission); + case ALGORITHM_M4A_NO_DASH: + return new M4aNoDash(mission); /*case "example-algorithm": - return new ExampleAlgorithm(mission);*/ + return new ExampleAlgorithm(mission);*/ default: throw new RuntimeException("Unimplemented post-processing algorithm: " + algorithmName); } @@ -65,7 +71,8 @@ public abstract class Postprocessing { public void run() throws IOException { File file = mission.getDownloadedFile(); - CircularFile out = null; + File temp = null; + CircularFileWriter out = null; int result; long finalLength = -1; @@ -81,29 +88,54 @@ public abstract class Postprocessing { } sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.getDownloadedFile().length(), "rw"); - int[] idx = {0}; - CircularFile.OffsetChecker checker = () -> { - while (idx[0] < sources.length) { - /* - * WARNING: never use rewind() in any chunk after any writing (especially on first chunks) - * or the CircularFile can lead to unexpected results - */ - if (sources[idx[0]].isDisposed() || sources[idx[0]].available() < 1) { - idx[0]++; - continue;// the selected source is not used anymore + if (test(sources)) { + for (SharpStream source : sources) source.rewind(); + + OffsetChecker checker = () -> { + for (ChunkFileInputStream source : sources) { + /* + * WARNING: never use rewind() in any chunk after any writing (especially on first chunks) + * or the CircularFileWriter can lead to unexpected results + */ + if (source.isDisposed() || source.available() < 1) { + continue;// the selected source is not used anymore + } + + return source.getFilePointer() - 1; } - return sources[idx[0]].getFilePointer() - 1; - } + return -1; + }; - return -1; - }; - out = new CircularFile(file, 0, this::progressReport, checker); + temp = new File(mission.location, mission.name + ".tmp"); - result = process(out, sources); + out = new CircularFileWriter(file, temp, checker); + out.onProgress = this::progressReport; - if (result == OK_RESULT) - finalLength = out.finalizeFile(); + out.onWriteError = (err) -> { + mission.postprocessingState = 3; + mission.notifyError(ERROR_POSTPROCESSING_HOLD, err); + + try { + synchronized (this) { + while (mission.postprocessingState == 3) + wait(); + } + } catch (InterruptedException e) { + // nothing to do + Log.e(this.getClass().getSimpleName(), "got InterruptedException"); + } + + return mission.errCode == ERROR_NOTHING; + }; + + result = process(out, sources); + + if (result == OK_RESULT) + finalLength = out.finalizeFile(); + } else { + result = OK_RESULT; + } } finally { for (SharpStream source : sources) { if (source != null && !source.isDisposed()) { @@ -113,17 +145,22 @@ public abstract class Postprocessing { if (out != null) { out.dispose(); } + if (temp != null) { + //noinspection ResultOfMethodCallIgnored + temp.delete(); + } } } else { - result = process(null); + result = test() ? process(null) : OK_RESULT; } if (result == OK_RESULT) { - if (finalLength < 0) finalLength = file.length(); - mission.done = finalLength; - mission.length = finalLength; + if (finalLength != -1) { + mission.done = finalLength; + mission.length = finalLength; + } } else { - mission.errCode = DownloadMission.ERROR_UNKNOWN_EXCEPTION; + mission.errCode = ERROR_UNKNOWN_EXCEPTION; mission.errObject = new RuntimeException("post-processing algorithm returned " + result); } @@ -134,7 +171,18 @@ public abstract class Postprocessing { } /** - * Abstract method to execute the pos-processing algorithm + * Test if the post-processing algorithm can be skipped + * + * @param sources files to be processed + * @return {@code true} if the post-processing is required, otherwise, {@code false} + * @throws IOException if an I/O error occurs. + */ + boolean test(SharpStream... sources) throws IOException { + return true; + } + + /** + * Abstract method to execute the post-processing algorithm * * @param out output stream * @param sources files to be processed @@ -151,7 +199,7 @@ public abstract class Postprocessing { return mission.postprocessingArgs[index]; } - void progressReport(long done) { + private void progressReport(long done) { mission.done = done; if (mission.length < mission.done) mission.length = mission.done; diff --git a/app/src/main/java/us/shandian/giga/postprocessing/io/ChunkFileInputStream.java b/app/src/main/java/us/shandian/giga/postprocessing/io/ChunkFileInputStream.java index cd62c5d22..ee2fcddd5 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/io/ChunkFileInputStream.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/io/ChunkFileInputStream.java @@ -94,7 +94,7 @@ public class ChunkFileInputStream extends SharpStream { } @Override - public int available() { + public long available() { return (int) (length - position); } @@ -147,7 +147,4 @@ public class ChunkFileInputStream extends SharpStream { public void write(byte[] buffer, int offset, int count) { } - @Override - public void flush() { - } } diff --git a/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFile.java b/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFile.java deleted file mode 100644 index d2fc82d33..000000000 --- a/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFile.java +++ /dev/null @@ -1,375 +0,0 @@ -package us.shandian.giga.postprocessing.io; - -import org.schabi.newpipe.streams.io.SharpStream; - -import java.io.File; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.util.ArrayList; - -public class CircularFile extends SharpStream { - - private final static int AUX_BUFFER_SIZE = 1024 * 1024;// 1 MiB - private final static int AUX_BUFFER_SIZE2 = 512 * 1024;// 512 KiB - private final static int NOTIFY_BYTES_INTERVAL = 64 * 1024;// 64 KiB - private final static int QUEUE_BUFFER_SIZE = 8 * 1024;// 8 KiB - private final static boolean IMMEDIATE_AUX_BUFFER_FLUSH = false; - - private RandomAccessFile out; - private long position; - private long maxLengthKnown = -1; - - private ArrayList auxiliaryBuffers; - private OffsetChecker callback; - private ManagedBuffer queue; - private long startOffset; - private ProgressReport onProgress; - private long reportPosition; - - public CircularFile(File file, long offset, ProgressReport progressReport, OffsetChecker checker) throws IOException { - if (checker == null) { - throw new NullPointerException("checker is null"); - } - - try { - queue = new ManagedBuffer(QUEUE_BUFFER_SIZE); - out = new RandomAccessFile(file, "rw"); - out.seek(offset); - position = offset; - } catch (IOException err) { - try { - if (out != null) { - out.close(); - } - } catch (IOException e) { - // nothing to do - } - throw err; - } - - auxiliaryBuffers = new ArrayList<>(15); - callback = checker; - startOffset = offset; - reportPosition = offset; - onProgress = progressReport; - - } - - /** - * Close the file without flushing any buffer - */ - @Override - public void dispose() { - try { - auxiliaryBuffers = null; - if (out != null) { - out.close(); - out = null; - } - } catch (IOException err) { - // nothing to do - } - } - - /** - * Flush any buffer and close the output file. Use this method if the - * operation is successful - * - * @return the final length of the file - * @throws IOException if an I/O error occurs - */ - public long finalizeFile() throws IOException { - flushEverything(); - - if (maxLengthKnown > -1) { - position = maxLengthKnown; - } - if (position < out.length()) { - out.setLength(position); - } - - dispose(); - - return position; - } - - @Override - public void write(byte b) throws IOException { - write(new byte[]{b}, 0, 1); - } - - @Override - public void write(byte b[]) throws IOException { - write(b, 0, b.length); - } - - @Override - public void write(byte b[], int off, int len) throws IOException { - if (len == 0) { - return; - } - - long end = callback.check(); - long available; - - if (end == -1) { - available = Long.MAX_VALUE; - } else { - if (end < startOffset) { - throw new IOException("The reported offset is invalid. reported offset is " + String.valueOf(end)); - } - available = end - position; - } - - // Check if possible flush one or more auxiliary buffer - if (auxiliaryBuffers.size() > 0) { - ManagedBuffer aux = auxiliaryBuffers.get(0); - - // check if there is enough space to flush it completely - while (available >= (aux.size + queue.size)) { - available -= aux.size; - writeQueue(aux.buffer, 0, aux.size); - aux.dereference(); - auxiliaryBuffers.remove(0); - - if (auxiliaryBuffers.size() < 1) { - aux = null; - break; - } - aux = auxiliaryBuffers.get(0); - } - - if (IMMEDIATE_AUX_BUFFER_FLUSH) { - // try partial flush to avoid allocate another auxiliary buffer - if (aux != null && aux.available() < len && available > queue.size) { - int size = Math.min(aux.size, (int) available - queue.size); - - writeQueue(aux.buffer, 0, size); - aux.dereference(size); - - available -= size; - } - } - } - - if (auxiliaryBuffers.size() < 1 && available > (len + queue.size)) { - writeQueue(b, off, len); - } else { - int i = auxiliaryBuffers.size() - 1; - while (len > 0) { - if (i < 0) { - // allocate a new auxiliary buffer - auxiliaryBuffers.add(new ManagedBuffer(AUX_BUFFER_SIZE)); - i++; - } - - ManagedBuffer aux = auxiliaryBuffers.get(i); - available = aux.available(); - - if (available < 1) { - // secondary auxiliary buffer - available = len; - aux = new ManagedBuffer(Math.max(len, AUX_BUFFER_SIZE2)); - auxiliaryBuffers.add(aux); - i++; - } else { - available = Math.min(len, available); - } - - aux.write(b, off, (int) available); - - len -= available; - if (len > 0) off += available; - } - } - } - - private void writeOutside(byte buffer[], int offset, int length) throws IOException { - out.write(buffer, offset, length); - position += length; - - if (onProgress != null && position > reportPosition) { - reportPosition = position + NOTIFY_BYTES_INTERVAL; - onProgress.report(position); - } - } - - private void writeQueue(byte[] buffer, int offset, int length) throws IOException { - while (length > 0) { - if (queue.available() < length) { - flushQueue(); - - if (length >= queue.buffer.length) { - writeOutside(buffer, offset, length); - return; - } - } - - int size = Math.min(queue.available(), length); - queue.write(buffer, offset, size); - - offset += size; - length -= size; - } - - if (queue.size >= queue.buffer.length) { - flushQueue(); - } - } - - private void flushQueue() throws IOException { - writeOutside(queue.buffer, 0, queue.size); - queue.size = 0; - } - - private void flushEverything() throws IOException { - flushQueue(); - - if (auxiliaryBuffers.size() > 0) { - for (ManagedBuffer aux : auxiliaryBuffers) { - writeOutside(aux.buffer, 0, aux.size); - aux.dereference(); - } - auxiliaryBuffers.clear(); - } - } - - /** - * Flush any buffer directly to the file. Warning: use this method ONLY if - * all read dependencies are disposed - * - * @throws IOException if the dependencies are not disposed - */ - @Override - public void flush() throws IOException { - if (callback.check() != -1) { - throw new IOException("All read dependencies of this file must be disposed first"); - } - flushEverything(); - - // Save the current file length in case the method {@code rewind()} is called - if (position > maxLengthKnown) { - maxLengthKnown = position; - } - } - - @Override - public void rewind() throws IOException { - flush(); - out.seek(startOffset); - - if (onProgress != null) { - onProgress.report(-position); - } - - position = startOffset; - reportPosition = startOffset; - - } - - @Override - public long skip(long amount) throws IOException { - flush(); - position += amount; - - out.seek(position); - - return amount; - } - - @Override - public boolean isDisposed() { - return out == null; - } - - @Override - public boolean canRewind() { - return true; - } - - @Override - public boolean canWrite() { - return true; - } - - // - @Override - public boolean canRead() { - return false; - } - - @Override - public int read() { - throw new UnsupportedOperationException("write-only"); - } - - @Override - public int read(byte[] buffer) { - throw new UnsupportedOperationException("write-only"); - } - - @Override - public int read(byte[] buffer, int offset, int count) { - throw new UnsupportedOperationException("write-only"); - } - - @Override - public int available() { - throw new UnsupportedOperationException("write-only"); - } -// - - public interface OffsetChecker { - - /** - * Checks the amount of available space ahead - * - * @return absolute offset in the file where no more data SHOULD NOT be - * written. If the value is -1 the whole file will be used - */ - long check(); - } - - public interface ProgressReport { - - void report(long progress); - } - - class ManagedBuffer { - - byte[] buffer; - int size; - - ManagedBuffer(int length) { - buffer = new byte[length]; - } - - void dereference() { - buffer = null; - size = 0; - } - - void dereference(int amount) { - if (amount > size) { - throw new IndexOutOfBoundsException("Invalid dereference amount (" + amount + ">=" + size + ")"); - } - size -= amount; - System.arraycopy(buffer, amount, buffer, 0, size); - } - - protected int available() { - return buffer.length - size; - } - - private void write(byte[] b, int off, int len) { - System.arraycopy(b, off, buffer, size, len); - size += len; - } - - @Override - public String toString() { - return "holding: " + String.valueOf(size) + " length: " + String.valueOf(buffer.length) + " available: " + String.valueOf(available()); - } - - } -} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFileWriter.java b/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFileWriter.java new file mode 100644 index 000000000..4c4160fa3 --- /dev/null +++ b/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFileWriter.java @@ -0,0 +1,459 @@ +package us.shandian.giga.postprocessing.io; + +import android.support.annotation.NonNull; + +import org.schabi.newpipe.streams.io.SharpStream; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; + +public class CircularFileWriter extends SharpStream { + + private final static int QUEUE_BUFFER_SIZE = 8 * 1024;// 8 KiB + private final static int NOTIFY_BYTES_INTERVAL = 64 * 1024;// 64 KiB + private final static int THRESHOLD_AUX_LENGTH = 3 * 1024 * 1024;// 3 MiB + + private OffsetChecker callback; + + public ProgressReport onProgress; + public WriteErrorHandle onWriteError; + + private long reportPosition; + private long maxLengthKnown = -1; + + private BufferedFile out; + private BufferedFile aux; + + public CircularFileWriter(File source, File temp, OffsetChecker checker) throws IOException { + if (checker == null) { + throw new NullPointerException("checker is null"); + } + + if (!temp.exists()) { + if (!temp.createNewFile()) { + throw new IOException("Cannot create a temporal file"); + } + } + + aux = new BufferedFile(temp); + out = new BufferedFile(source); + + callback = checker; + + reportPosition = NOTIFY_BYTES_INTERVAL; + } + + private void flushAuxiliar() throws IOException { + if (aux.length < 1) { + return; + } + + boolean underflow = out.getOffset() >= out.length; + + out.flush(); + aux.flush(); + + aux.target.seek(0); + out.target.seek(out.length); + + long length = aux.length; + out.length += aux.length; + + while (length > 0) { + int read = (int) Math.min(length, Integer.MAX_VALUE); + read = aux.target.read(aux.queue, 0, Math.min(read, aux.queue.length)); + + out.writeProof(aux.queue, read); + length -= read; + } + + if (underflow) { + out.offset += aux.offset; + out.target.seek(out.offset); + } else { + out.offset = out.length; + } + + if (out.length > maxLengthKnown) { + maxLengthKnown = out.length; + } + + if (aux.length > THRESHOLD_AUX_LENGTH) { + aux.target.setLength(THRESHOLD_AUX_LENGTH);// or setLength(0); + } + + aux.reset(); + } + + /** + * Flush any buffer and close the output file. Use this method if the + * operation is successful + * + * @return the final length of the file + * @throws IOException if an I/O error occurs + */ + public long finalizeFile() throws IOException { + flushAuxiliar(); + + out.flush(); + + // change file length (if required) + long length = Math.max(maxLengthKnown, out.length); + if (length != out.target.length()) { + out.target.setLength(length); + } + + dispose(); + + return length; + } + + /** + * Close the file without flushing any buffer + */ + @Override + public void dispose() { + if (out != null) { + out.dispose(); + out = null; + } + if (aux != null) { + aux.dispose(); + aux = null; + } + } + + @Override + public void write(byte b) throws IOException { + write(new byte[]{b}, 0, 1); + } + + @Override + public void write(byte b[]) throws IOException { + write(b, 0, b.length); + } + + @Override + public void write(byte b[], int off, int len) throws IOException { + if (len == 0) { + return; + } + + long available; + long offsetOut = out.getOffset(); + long offsetAux = aux.getOffset(); + long end = callback.check(); + + if (end == -1) { + available = Integer.MAX_VALUE; + } else if (end < offsetOut) { + throw new IOException("The reported offset is invalid: " + String.valueOf(offsetOut)); + } else { + available = end - offsetOut; + } + + boolean usingAux = aux.length > 0 && offsetOut >= out.length; + boolean underflow = offsetAux < aux.length || offsetOut < out.length; + + if (usingAux) { + // before continue calculate the final length of aux + long length = offsetAux + len; + if (underflow) { + if (aux.length > length) { + length = aux.length;// the length is not changed + } + } else { + length = aux.length + len; + } + + if (length > available || length < THRESHOLD_AUX_LENGTH) { + aux.write(b, off, len); + } else { + if (underflow) { + aux.write(b, off, len); + flushAuxiliar(); + } else { + flushAuxiliar(); + out.write(b, off, len);// write directly on the output + } + } + } else { + if (underflow) { + available = out.length - offsetOut; + } + + int length = Math.min(len, (int) available); + out.write(b, off, length); + + len -= length; + off += length; + + if (len > 0) { + aux.write(b, off, len); + } + } + + if (onProgress != null) { + long absoluteOffset = out.getOffset() + aux.getOffset(); + if (absoluteOffset > reportPosition) { + reportPosition = absoluteOffset + NOTIFY_BYTES_INTERVAL; + onProgress.report(absoluteOffset); + } + } + } + + @Override + public void flush() throws IOException { + aux.flush(); + out.flush(); + + long total = out.length + aux.length; + if (total > maxLengthKnown) { + maxLengthKnown = total;// save the current file length in case the method {@code rewind()} is called + } + } + + @Override + public long skip(long amount) throws IOException { + seek(out.getOffset() + aux.getOffset() + amount); + return amount; + } + + @Override + public void rewind() throws IOException { + if (onProgress != null) { + onProgress.report(-out.length - aux.length);// rollback the whole progress + } + + seek(0); + + reportPosition = NOTIFY_BYTES_INTERVAL; + } + + @Override + public void seek(long offset) throws IOException { + long total = out.length + aux.length; + if (offset == total) { + return;// nothing to do + } + + // flush everything, avoid any underflow + flush(); + + if (offset < 0 || offset > total) { + throw new IOException("desired offset is outside of range=0-" + total + " offset=" + offset); + } + + if (offset > out.length) { + out.seek(out.length); + aux.seek(offset - out.length); + } else { + out.seek(offset); + aux.seek(0); + } + } + + @Override + public boolean isDisposed() { + return out == null; + } + + @Override + public boolean canRewind() { + return true; + } + + @Override + public boolean canWrite() { + return true; + } + + @Override + public boolean canSeek() { + return true; + } + + // + @Override + public boolean canRead() { + return false; + } + + @Override + public int read() { + throw new UnsupportedOperationException("write-only"); + } + + @Override + public int read(byte[] buffer + ) { + throw new UnsupportedOperationException("write-only"); + } + + @Override + public int read(byte[] buffer, int offset, int count + ) { + throw new UnsupportedOperationException("write-only"); + } + + @Override + public long available() { + throw new UnsupportedOperationException("write-only"); + } + // + + public interface OffsetChecker { + + /** + * Checks the amount of available space ahead + * + * @return absolute offset in the file where no more data SHOULD NOT be + * written. If the value is -1 the whole file will be used + */ + long check(); + } + + public interface ProgressReport { + + /** + * Report the size of the new file + * + * @param progress the new size + */ + void report(long progress); + } + + public interface WriteErrorHandle { + + /** + * Attempts to handle a I/O exception + * + * @param err the cause + * @return {@code true} to retry and continue, otherwise, {@code false} + * and throw the exception + */ + boolean handle(Exception err); + } + + class BufferedFile { + + protected final RandomAccessFile target; + + private long offset; + protected long length; + + private byte[] queue; + private int queueSize; + + BufferedFile(File file) throws FileNotFoundException { + queue = new byte[QUEUE_BUFFER_SIZE]; + target = new RandomAccessFile(file, "rw"); + } + + protected long getOffset() { + return offset + queueSize;// absolute offset in the file + } + + protected void dispose() { + try { + queue = null; + target.close(); + } catch (IOException e) { + // nothing to do + } + } + + protected void write(byte b[], int off, int len) throws IOException { + while (len > 0) { + // if the queue is full, the method available() will flush the queue + int read = Math.min(available(), len); + + // enqueue incoming buffer + System.arraycopy(b, off, queue, queueSize, read); + queueSize += read; + + len -= read; + off += read; + } + + long total = offset + queueSize; + if (total > length) { + length = total;// save length + } + } + + protected void flush() throws IOException { + writeProof(queue, queueSize); + offset += queueSize; + queueSize = 0; + } + + protected void rewind() throws IOException { + offset = 0; + target.seek(0); + } + + protected int available() throws IOException { + if (queueSize >= queue.length) { + flush(); + return queue.length; + } + + return queue.length - queueSize; + } + + protected void reset() throws IOException { + offset = 0; + length = 0; + target.seek(0); + } + + protected void seek(long absoluteOffset) throws IOException { + offset = absoluteOffset; + target.seek(absoluteOffset); + } + + protected void writeProof(byte[] buffer, int length) throws IOException { + if (onWriteError == null) { + target.write(buffer, 0, length); + return; + } + + while (true) { + try { + target.write(buffer, 0, length); + return; + } catch (Exception e) { + if (!onWriteError.handle(e)) { + throw e;// give up + } + } + } + } + + @NonNull + @Override + public String toString() { + String absOffset; + String absLength; + + try { + absOffset = Long.toString(target.getFilePointer()); + } catch (IOException e) { + absOffset = "[" + e.getLocalizedMessage() + "]"; + } + try { + absLength = Long.toString(target.length()); + } catch (IOException e) { + absLength = "[" + e.getLocalizedMessage() + "]"; + } + + return String.format( + "offset=%s length=%s queue=%s absOffset=%s absLength=%s", + offset, length, queueSize, absOffset, absLength + ); + } + } +} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/io/FileStream.java b/app/src/main/java/us/shandian/giga/postprocessing/io/FileStream.java deleted file mode 100644 index c1b675eef..000000000 --- a/app/src/main/java/us/shandian/giga/postprocessing/io/FileStream.java +++ /dev/null @@ -1,126 +0,0 @@ -package us.shandian.giga.postprocessing.io; - -import org.schabi.newpipe.streams.io.SharpStream; - -import java.io.IOException; -import java.io.RandomAccessFile; -import java.nio.channels.FileChannel; - -/** - * @author kapodamy - */ -public class FileStream extends SharpStream { - - public enum Mode { - Read, - ReadWrite - } - - public RandomAccessFile source; - private final Mode mode; - - public FileStream(String path, Mode mode) throws IOException { - String flags; - - if (mode == Mode.Read) { - flags = "r"; - } else { - flags = "rw"; - } - - this.mode = mode; - source = new RandomAccessFile(path, flags); - } - - @Override - public int read() throws IOException { - return source.read(); - } - - @Override - public int read(byte b[]) throws IOException { - return read(b, 0, b.length); - } - - @Override - public int read(byte b[], int off, int len) throws IOException { - return source.read(b, off, len); - } - - @Override - public long skip(long pos) throws IOException { - FileChannel fc = source.getChannel(); - fc.position(fc.position() + pos); - return pos; - } - - @Override - public int available() { - try { - return (int) (source.length() - source.getFilePointer()); - } catch (IOException ex) { - return 0; - } - } - - @SuppressWarnings("EmptyCatchBlock") - @Override - public void dispose() { - try { - source.close(); - } catch (IOException err) { - - } finally { - source = null; - } - } - - @Override - public boolean isDisposed() { - return source == null; - } - - @Override - public void rewind() throws IOException { - source.getChannel().position(0); - } - - @Override - public boolean canRewind() { - return true; - } - - @Override - public boolean canRead() { - return mode == Mode.Read || mode == Mode.ReadWrite; - } - - @Override - public boolean canWrite() { - return mode == Mode.ReadWrite; - } - - @Override - public void write(byte value) throws IOException { - source.write(value); - } - - @Override - public void write(byte[] buffer) throws IOException { - source.write(buffer); - } - - @Override - public void write(byte[] buffer, int offset, int count) throws IOException { - source.write(buffer, offset, count); - } - - @Override - public void flush() { - } - - @Override - public void setLength(long length) throws IOException { - source.setLength(length); - } -} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/io/SharpInputStream.java b/app/src/main/java/us/shandian/giga/postprocessing/io/SharpInputStream.java index 52e0775da..586456d98 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/io/SharpInputStream.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/io/SharpInputStream.java @@ -14,6 +14,7 @@ import java.io.InputStream; /** * Wrapper for the classic {@link java.io.InputStream} + * * @author kapodamy */ public class SharpInputStream extends InputStream { @@ -49,7 +50,8 @@ public class SharpInputStream extends InputStream { @Override public int available() { - return base.available(); + long res = base.available(); + return res > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) res; } @Override diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManager.java b/app/src/main/java/us/shandian/giga/service/DownloadManager.java index 883c26850..58246beb1 100644 --- a/app/src/main/java/us/shandian/giga/service/DownloadManager.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManager.java @@ -21,6 +21,8 @@ import us.shandian.giga.get.DownloadMission; import us.shandian.giga.get.FinishedMission; import us.shandian.giga.get.Mission; import us.shandian.giga.get.sqlite.DownloadDataSource; +import us.shandian.giga.service.DownloadManagerService.DMChecker; +import us.shandian.giga.service.DownloadManagerService.MissionCheck; import us.shandian.giga.util.Utility; import static org.schabi.newpipe.BuildConfig.DEBUG; @@ -28,7 +30,7 @@ import static org.schabi.newpipe.BuildConfig.DEBUG; public class DownloadManager { private static final String TAG = DownloadManager.class.getSimpleName(); - enum NetworkState {Unavailable, WifiOperating, MobileOperating, OtherOperating} + enum NetworkState {Unavailable, Operating, MeteredOperating} public final static int SPECIAL_NOTHING = 0; public final static int SPECIAL_PENDING = 1; @@ -45,7 +47,9 @@ public class DownloadManager { private NetworkState mLastNetworkStatus = NetworkState.Unavailable; int mPrefMaxRetry; - boolean mPrefCrossNetwork; + boolean mPrefMeteredDownloads; + boolean mPrefQueueLimit; + private boolean mSelfMissionsControl; /** * Create a new instance @@ -152,8 +156,8 @@ public class DownloadManager { } mis.postprocessingState = 0; - mis.errCode = DownloadMission.ERROR_POSTPROCESSING; - mis.errObject = new RuntimeException("stopped unexpectedly"); + mis.errCode = DownloadMission.ERROR_POSTPROCESSING_STOPPED; + mis.errObject = null; } else if (exists && !dl.isFile()) { // probably a folder, this should never happens if (!sub.delete()) { @@ -162,20 +166,21 @@ public class DownloadManager { continue; } - if (!exists) { + if (!exists && mis.isInitialized()) { // downloaded file deleted, reset mission state DownloadMission m = new DownloadMission(mis.urls, mis.name, mis.location, mis.kind, mis.postprocessingName, mis.postprocessingArgs); m.timestamp = mis.timestamp; m.threadCount = mis.threadCount; m.source = mis.source; - m.maxRetry = mis.maxRetry; m.nearLength = mis.nearLength; + m.setEnqueued(mis.enqueued); mis = m; } mis.running = false; mis.recovered = exists; mis.metadata = sub; + mis.maxRetry = mPrefMaxRetry; mis.mHandler = mHandler; mMissionsPending.add(mis); @@ -205,17 +210,25 @@ public class DownloadManager { synchronized (this) { // check for existing pending download DownloadMission pendingMission = getPendingMission(location, name); + if (pendingMission != null) { - // generate unique filename (?) - try { - name = generateUniqueName(location, name); - } catch (Exception e) { - Log.e(TAG, "Unable to generate unique name", e); - name = System.currentTimeMillis() + name; - Log.i(TAG, "Using " + name); + if (pendingMission.running) { + // generate unique filename (?) + try { + name = generateUniqueName(location, name); + } catch (Exception e) { + Log.e(TAG, "Unable to generate unique name", e); + name = System.currentTimeMillis() + name; + Log.i(TAG, "Using " + name); + } + } else { + // dispose the mission + mMissionsPending.remove(pendingMission); + mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED); + pendingMission.delete(); } } else { - // check for existing finished download + // check for existing finished download and dispose (if exists) int index = getFinishedMissionIndex(location, name); if (index >= 0) mDownloadDataSource.deleteMission(mMissionsFinished.remove(index)); } @@ -242,14 +255,17 @@ public class DownloadManager { mission.timestamp = System.currentTimeMillis(); } + mSelfMissionsControl = true; mMissionsPending.add(mission); - // Before starting, save the state in case the internet connection is not available + // Before continue, save the metadata in case the internet connection is not available Utility.writeToFile(mission.metadata, mission); - if (canDownloadInCurrentNetwork() && (getRunningMissionsCount() < 1)) { + boolean start = !mPrefQueueLimit || getRunningMissionsCount() < 1; + + if (canDownloadInCurrentNetwork() && start) { + mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS); mission.start(); - mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_RUNNING); } } } @@ -257,13 +273,14 @@ public class DownloadManager { public void resumeMission(DownloadMission mission) { if (!mission.running) { + mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS); mission.start(); - mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_RUNNING); } } public void pauseMission(DownloadMission mission) { if (mission.running) { + mission.setEnqueued(false); mission.pause(); mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED); } @@ -335,7 +352,7 @@ public class DownloadManager { int count = 0; synchronized (this) { for (DownloadMission mission : mMissionsPending) { - if (mission.running && !mission.isFinished() && !mission.isPsFailed()) + if (mission.running && !mission.isPsFailed() && !mission.isFinished()) count++; } } @@ -343,10 +360,36 @@ public class DownloadManager { return count; } - void pauseAllMissions() { + public void pauseAllMissions(boolean force) { + boolean flag = false; + synchronized (this) { - for (DownloadMission mission : mMissionsPending) mission.pause(); + for (DownloadMission mission : mMissionsPending) { + if (!mission.running || mission.isPsRunning() || mission.isFinished()) continue; + + if (force) mission.threads = null;// avoid waiting for threads + + mission.pause(); + flag = true; + } } + + if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED); + } + + public void startAllMissions() { + boolean flag = false; + + synchronized (this) { + for (DownloadMission mission : mMissionsPending) { + if (mission.running || mission.isPsFailed() || mission.isFinished()) continue; + + flag = true; + mission.start(); + } + } + + if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS); } @@ -415,31 +458,35 @@ public class DownloadManager { } /** - * runs another mission in queue if possible + * runs one or multiple missions in from queue if possible * - * @return true if exits pending missions running or a mission was started, otherwise, false + * @return true if one or multiple missions are running, otherwise, false */ - boolean runAnotherMission() { + boolean runMissions() { synchronized (this) { if (mMissionsPending.size() < 1) return false; - - int i = getRunningMissionsCount(); - if (i > 0) return true; - if (!canDownloadInCurrentNetwork()) return false; - for (DownloadMission mission : mMissionsPending) { - if (!mission.running && mission.errCode == DownloadMission.ERROR_NOTHING && mission.enqueued) { - resumeMission(mission); - return true; - } + if (mPrefQueueLimit) { + for (DownloadMission mission : mMissionsPending) + if (!mission.isFinished() && mission.running) return true; } - return false; + boolean flag = false; + for (DownloadMission mission : mMissionsPending) { + if (mission.running || !mission.enqueued || mission.isFinished()) continue; + + resumeMission(mission); + if (mPrefQueueLimit) return true; + flag = true; + } + + return flag; } } public MissionIterator getIterator() { + mSelfMissionsControl = true; return new MissionIterator(); } @@ -457,31 +504,43 @@ public class DownloadManager { private boolean canDownloadInCurrentNetwork() { if (mLastNetworkStatus == NetworkState.Unavailable) return false; - return !(mPrefCrossNetwork && mLastNetworkStatus == NetworkState.MobileOperating); + return !(mPrefMeteredDownloads && mLastNetworkStatus == NetworkState.MeteredOperating); } - void handleConnectivityChange(NetworkState currentStatus) { + void handleConnectivityState(NetworkState currentStatus, boolean updateOnly) { if (currentStatus == mLastNetworkStatus) return; mLastNetworkStatus = currentStatus; + if (currentStatus == NetworkState.Unavailable) return; - if (currentStatus == NetworkState.Unavailable) { - return; - } else if (currentStatus != NetworkState.MobileOperating || !mPrefCrossNetwork) { - return; + if (!mSelfMissionsControl || updateOnly) { + return;// don't touch anything without the user interaction } - boolean flag = false; + boolean isMetered = mPrefMeteredDownloads && mLastNetworkStatus == NetworkState.MeteredOperating; + + int running = 0; + int paused = 0; synchronized (this) { for (DownloadMission mission : mMissionsPending) { - if (mission.running && !mission.isFinished() && !mission.isPsRunning()) { - flag = true; + if (mission.isFinished() || mission.isPsRunning()) continue; + + if (mission.running && isMetered) { + paused++; mission.pause(); + } else if (!mission.running && !isMetered && mission.enqueued) { + running++; + mission.start(); + if (mPrefQueueLimit) break; } } } - if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED); + if (running > 0) { + mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS); + return; + } + if (paused > 0) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PAUSED); } void updateMaximumAttempts() { @@ -506,21 +565,24 @@ public class DownloadManager { ), Toast.LENGTH_LONG).show(); } - void checkForRunningMission(String location, String name, DownloadManagerService.DMChecker check) { - boolean listed; - boolean finished = false; + void checkForRunningMission(String location, String name, DMChecker check) { + MissionCheck result = MissionCheck.None; synchronized (this) { - DownloadMission mission = getPendingMission(location, name); - if (mission != null) { - listed = true; + DownloadMission pending = getPendingMission(location, name); + + if (pending == null) { + if (getFinishedMissionIndex(location, name) >= 0) result = MissionCheck.Finished; } else { - listed = getFinishedMissionIndex(location, name) >= 0; - finished = listed; + if (pending.isFinished()) { + result = MissionCheck.Finished;// this never should happen (race-condition) + } else { + result = pending.running ? MissionCheck.PendingRunning : MissionCheck.Pending; + } } } - check.callback(listed, finished); + check.callback(result); } public class MissionIterator extends DiffUtil.Callback { @@ -592,39 +654,6 @@ public class DownloadManager { return SPECIAL_NOTHING; } - public MissionItem getItemUnsafe(int position) { - synchronized (DownloadManager.this) { - int count = mMissionsPending.size(); - int count2 = mMissionsFinished.size(); - - if (count > 0) { - position--; - if (position == -1) - return new MissionItem(SPECIAL_PENDING); - else if (position < count) - return new MissionItem(SPECIAL_NOTHING, mMissionsPending.get(position)); - else if (position == count && count2 > 0) - return new MissionItem(SPECIAL_FINISHED); - else - position -= count; - } else { - if (count2 > 0 && position == 0) { - return new MissionItem(SPECIAL_FINISHED); - } - } - - position--; - - if (count2 < 1) { - throw new RuntimeException( - String.format("Out of range. pending_count=%s finished_count=%s position=%s", count, count2, position) - ); - } - - return new MissionItem(SPECIAL_NOTHING, mMissionsFinished.get(position)); - } - } - public void start() { current = getSpecialItems(); @@ -647,6 +676,32 @@ public class DownloadManager { return hasFinished; } + /** + * Check if exists missions running and paused. Corrupted and hidden missions are not counted + * + * @return two-dimensional array contains the current missions state. + * 1° entry: true if has at least one mission running + * 2° entry: true if has at least one mission paused + */ + public boolean[] hasValidPendingMissions() { + boolean running = false; + boolean paused = false; + + synchronized (DownloadManager.this) { + for (DownloadMission mission : mMissionsPending) { + if (hidden.contains(mission) || mission.isPsFailed() || mission.isFinished()) + continue; + + if (mission.running) + paused = true; + else + running = true; + } + } + + return new boolean[]{running, paused}; + } + @Override public int getOldListSize() { diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java index a57fe1734..be1e20dd6 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -15,7 +15,9 @@ import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.ConnectivityManager; +import android.net.Network; import android.net.NetworkInfo; +import android.net.NetworkRequest; import android.net.Uri; import android.os.Binder; import android.os.Build; @@ -24,6 +26,7 @@ import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.preference.PreferenceManager; +import android.support.annotation.NonNull; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat.Builder; import android.support.v4.content.PermissionChecker; @@ -48,7 +51,6 @@ public class DownloadManagerService extends Service { private static final String TAG = "DownloadManagerService"; - public static final int MESSAGE_RUNNING = 0; public static final int MESSAGE_PAUSED = 1; public static final int MESSAGE_FINISHED = 2; public static final int MESSAGE_PROGRESS = 3; @@ -76,7 +78,7 @@ public class DownloadManagerService extends Service { private Notification mNotification; private Handler mHandler; private boolean mForeground = false; - private NotificationManager notificationManager = null; + private NotificationManager mNotificationManager = null; private boolean mDownloadNotificationEnable = true; private int downloadDoneCount = 0; @@ -85,7 +87,9 @@ public class DownloadManagerService extends Service { private final ArrayList mEchoObservers = new ArrayList<>(1); - private BroadcastReceiver mNetworkStateListener; + private ConnectivityManager mConnectivityManager; + private BroadcastReceiver mNetworkStateListener = null; + private ConnectivityManager.NetworkCallback mNetworkStateListenerL = null; private SharedPreferences mPrefs = null; private final SharedPreferences.OnSharedPreferenceChangeListener mPrefChangeListener = this::handlePreferenceChange; @@ -147,25 +151,39 @@ public class DownloadManagerService extends Service { .setContentText(getString(R.string.msg_running_detail)); mNotification = builder.build(); - notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - mNetworkStateListener = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false)) { - handleConnectivityChange(null); - return; + mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + mConnectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + mNetworkStateListenerL = new ConnectivityManager.NetworkCallback() { + @Override + public void onAvailable(Network network) { + handleConnectivityState(false); } - handleConnectivityChange(intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO)); - } - }; - registerReceiver(mNetworkStateListener, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); + + @Override + public void onLost(Network network) { + handleConnectivityState(false); + } + }; + mConnectivityManager.registerNetworkCallback(new NetworkRequest.Builder().build(), mNetworkStateListenerL); + } else { + mNetworkStateListener = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + handleConnectivityState(false); + } + }; + registerReceiver(mNetworkStateListener, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); + } mPrefs = PreferenceManager.getDefaultSharedPreferences(this); mPrefs.registerOnSharedPreferenceChangeListener(mPrefChangeListener); handlePreferenceChange(mPrefs, getString(R.string.downloads_cross_network)); handlePreferenceChange(mPrefs, getString(R.string.downloads_maximum_retry)); + handlePreferenceChange(mPrefs, getString(R.string.downloads_queue_limit)); mLock = new LockManager(this); } @@ -173,12 +191,11 @@ public class DownloadManagerService extends Service { @Override public int onStartCommand(Intent intent, int flags, int startId) { if (DEBUG) { - if (intent == null) { - Log.d(TAG, "Restarting"); - return START_NOT_STICKY; - } - Log.d(TAG, "Starting"); + Log.d(TAG, intent == null ? "Restarting" : "Starting"); } + + if (intent == null) return START_NOT_STICKY; + Log.i(TAG, "Got intent: " + intent); String action = intent.getAction(); if (action != null) { @@ -193,6 +210,8 @@ public class DownloadManagerService extends Service { String source = intent.getStringExtra(EXTRA_SOURCE); long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0); + handleConnectivityState(true);// first check the actual network status + mHandler.post(() -> mManager.startMission(urls, location, name, kind, threads, source, psName, psArgs, nearLength)); } else if (downloadDoneNotification != null) { @@ -221,21 +240,25 @@ public class DownloadManagerService extends Service { stopForeground(true); - if (notificationManager != null && downloadDoneNotification != null) { + if (mNotificationManager != null && downloadDoneNotification != null) { downloadDoneNotification.setDeleteIntent(null);// prevent NewPipe running when is killed, cleared from recent, etc - notificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build()); + mNotificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build()); } - mManager.pauseAllMissions(); - manageLock(false); - unregisterReceiver(mNetworkStateListener); + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + mConnectivityManager.unregisterNetworkCallback(mNetworkStateListenerL); + else + unregisterReceiver(mNetworkStateListener); + mPrefs.unregisterOnSharedPreferenceChangeListener(mPrefChangeListener); if (icDownloadDone != null) icDownloadDone.recycle(); if (icDownloadFailed != null) icDownloadFailed.recycle(); if (icLauncher != null) icLauncher.recycle(); + + mManager.pauseAllMissions(true); } @Override @@ -264,15 +287,16 @@ public class DownloadManagerService extends Service { notifyMediaScanner(mission.getDownloadedFile()); notifyFinishedDownload(mission.name); mManager.setFinished(mission); - updateForegroundState(mManager.runAnotherMission()); + handleConnectivityState(false); + updateForegroundState(mManager.runMissions()); break; - case MESSAGE_RUNNING: case MESSAGE_PROGRESS: updateForegroundState(true); break; case MESSAGE_ERROR: notifyFailedDownload(mission); - updateForegroundState(mManager.runAnotherMission()); + handleConnectivityState(false); + updateForegroundState(mManager.runMissions()); break; case MESSAGE_PAUSED: updateForegroundState(mManager.getRunningMissionsCount() > 0); @@ -293,36 +317,30 @@ public class DownloadManagerService extends Service { } } - private void handleConnectivityChange(NetworkInfo info) { + private void handleConnectivityState(boolean updateOnly) { + NetworkInfo info = mConnectivityManager.getActiveNetworkInfo(); NetworkState status; if (info == null) { status = NetworkState.Unavailable; - Log.i(TAG, "actual connectivity status is unavailable"); - } else if (!info.isAvailable() || !info.isConnected()) { - status = NetworkState.Unavailable; - Log.i(TAG, "actual connectivity status is not available and not connected"); + Log.i(TAG, "Active network [connectivity is unavailable]"); } else { - int type = info.getType(); - if (type == ConnectivityManager.TYPE_MOBILE || type == ConnectivityManager.TYPE_MOBILE_DUN) { - status = NetworkState.MobileOperating; - } else if (type == ConnectivityManager.TYPE_WIFI) { - status = NetworkState.WifiOperating; - } else if (type == ConnectivityManager.TYPE_WIMAX || - type == ConnectivityManager.TYPE_ETHERNET || - type == ConnectivityManager.TYPE_BLUETOOTH) { - status = NetworkState.OtherOperating; - } else { + boolean connected = info.isConnected(); + boolean metered = mConnectivityManager.isActiveNetworkMetered(); + + if (connected) + status = metered ? NetworkState.MeteredOperating : NetworkState.Operating; + else status = NetworkState.Unavailable; - } - Log.i(TAG, "actual connectivity status is " + status.name()); + + Log.i(TAG, "Active network [connected=" + connected + " metered=" + metered + "] " + info.toString()); } if (mManager == null) return;// avoid race-conditions while the service is starting - mManager.handleConnectivityChange(status); + mManager.handleConnectivityState(status, updateOnly); } - private void handlePreferenceChange(SharedPreferences prefs, String key) { + private void handlePreferenceChange(SharedPreferences prefs, @NonNull String key) { if (key.equals(getString(R.string.downloads_maximum_retry))) { try { String value = prefs.getString(key, getString(R.string.downloads_maximum_retry_default)); @@ -332,7 +350,9 @@ public class DownloadManagerService extends Service { } mManager.updateMaximumAttempts(); } else if (key.equals(getString(R.string.downloads_cross_network))) { - mManager.mPrefCrossNetwork = prefs.getBoolean(key, false); + mManager.mPrefMeteredDownloads = prefs.getBoolean(key, false); + } else if (key.equals(getString(R.string.downloads_queue_limit))) { + mManager.mPrefQueueLimit = prefs.getBoolean(key, true); } } @@ -366,19 +386,20 @@ public class DownloadManagerService extends Service { context.startService(intent); } - public static void checkForRunningMission(Context context, String location, String name, DMChecker check) { + public static void checkForRunningMission(Context context, String location, String name, DMChecker checker) { Intent intent = new Intent(); intent.setClass(context, DownloadManagerService.class); + context.startService(intent); + context.bindService(intent, new ServiceConnection() { @Override public void onServiceConnected(ComponentName cname, IBinder service) { try { - ((DMBinder) service).getDownloadManager().checkForRunningMission(location, name, check); + ((DMBinder) service).getDownloadManager().checkForRunningMission(location, name, checker); } catch (Exception err) { Log.w(TAG, "checkForRunningMission() callback is defective", err); } - // TODO: find a efficient way to unbind the service. This destroy the service due idle, but is started again when the user start a download. context.unbindService(this); } @@ -389,7 +410,7 @@ public class DownloadManagerService extends Service { } public void notifyFinishedDownload(String name) { - if (!mDownloadNotificationEnable || notificationManager == null) { + if (!mDownloadNotificationEnable || mNotificationManager == null) { return; } @@ -428,7 +449,7 @@ public class DownloadManagerService extends Service { downloadDoneNotification.setContentText(downloadDoneList); } - notificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build()); + mNotificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build()); downloadDoneCount++; } @@ -458,7 +479,7 @@ public class DownloadManagerService extends Service { .bigText(mission.name)); } - notificationManager.notify(id, downloadFailedNotification.build()); + mNotificationManager.notify(id, downloadFailedNotification.build()); } private PendingIntent makePendingIntent(String action) { @@ -487,7 +508,11 @@ public class DownloadManagerService extends Service { mLockAcquired = acquire; } - // Wrapper of DownloadManager + + //////////////////////////////////////////////////////////////////////////////////////////////// + // Wrappers for DownloadManager + //////////////////////////////////////////////////////////////////////////////////////////////// + public class DMBinder extends Binder { public DownloadManager getDownloadManager() { return mManager; @@ -502,15 +527,15 @@ public class DownloadManagerService extends Service { } public void clearDownloadNotifications() { - if (notificationManager == null) return; + if (mNotificationManager == null) return; if (downloadDoneNotification != null) { - notificationManager.cancel(DOWNLOADS_NOTIFICATION_ID); + mNotificationManager.cancel(DOWNLOADS_NOTIFICATION_ID); downloadDoneList.setLength(0); downloadDoneCount = 0; } if (downloadFailedNotification != null) { for (; downloadFailedNotificationID > DOWNLOADS_NOTIFICATION_ID; downloadFailedNotificationID--) { - notificationManager.cancel(downloadFailedNotificationID); + mNotificationManager.cancel(downloadFailedNotificationID); } mFailedDownloads.clear(); downloadFailedNotificationID++; @@ -524,7 +549,9 @@ public class DownloadManagerService extends Service { } public interface DMChecker { - void callback(boolean listed, boolean finished); + void callback(MissionCheck result); } + public enum MissionCheck {None, Pending, PendingRunning, Finished} + } diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java index 4a35aa166..cada3aeb8 100644 --- a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java +++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java @@ -14,15 +14,17 @@ import android.os.Looper; import android.os.Message; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.StringRes; import android.support.v4.content.FileProvider; import android.support.v4.view.ViewCompat; import android.support.v7.app.AlertDialog; import android.support.v7.util.DiffUtil; import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.RecyclerView.ViewHolder; import android.support.v7.widget.RecyclerView.Adapter; +import android.support.v7.widget.RecyclerView.ViewHolder; import android.util.Log; import android.util.SparseArray; +import android.view.HapticFeedbackConstants; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; @@ -36,14 +38,17 @@ import android.widget.Toast; import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.R; +import org.schabi.newpipe.report.ErrorActivity; +import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.NavigationHelper; -import java.io.File; import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.Collections; import us.shandian.giga.get.DownloadMission; import us.shandian.giga.get.FinishedMission; +import us.shandian.giga.get.Mission; import us.shandian.giga.service.DownloadManager; import us.shandian.giga.service.DownloadManagerService; import us.shandian.giga.ui.common.Deleter; @@ -57,10 +62,13 @@ import static us.shandian.giga.get.DownloadMission.ERROR_CONNECT_HOST; import static us.shandian.giga.get.DownloadMission.ERROR_FILE_CREATION; import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_NO_CONTENT; import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_UNSUPPORTED_RANGE; +import static us.shandian.giga.get.DownloadMission.ERROR_INSUFFICIENT_STORAGE; import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING; import static us.shandian.giga.get.DownloadMission.ERROR_PATH_CREATION; import static us.shandian.giga.get.DownloadMission.ERROR_PERMISSION_DENIED; import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING; +import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD; +import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_STOPPED; import static us.shandian.giga.get.DownloadMission.ERROR_SSL_EXCEPTION; import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION; import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_HOST; @@ -69,6 +77,7 @@ public class MissionAdapter extends Adapter { private static final SparseArray ALGORITHMS = new SparseArray<>(); private static final String TAG = "MissionAdapter"; private static final String UNDEFINED_PROGRESS = "--.-%"; + private static final String DEFAULT_MIME_TYPE = "*/*"; static { @@ -85,9 +94,11 @@ public class MissionAdapter extends Adapter { private ArrayList mPendingDownloadsItems = new ArrayList<>(); private Handler mHandler; private MenuItem mClear; + private MenuItem mStartButton; + private MenuItem mPauseButton; private View mEmptyMessage; - public MissionAdapter(Context context, DownloadManager downloadManager, MenuItem clearButton, View emptyMessage) { + public MissionAdapter(Context context, DownloadManager downloadManager, View emptyMessage) { mContext = context; mDownloadManager = downloadManager; mDeleter = null; @@ -105,10 +116,18 @@ public class MissionAdapter extends Adapter { onServiceMessage(msg); break; } + + if (mStartButton != null && mPauseButton != null) switch (msg.what) { + case DownloadManagerService.MESSAGE_DELETED: + case DownloadManagerService.MESSAGE_ERROR: + case DownloadManagerService.MESSAGE_FINISHED: + case DownloadManagerService.MESSAGE_PAUSED: + checkMasterButtonsVisibility(); + break; + } } }; - mClear = clearButton; mEmptyMessage = emptyMessage; mIterator = downloadManager.getIterator(); @@ -225,8 +244,10 @@ public class MissionAdapter extends Adapter { long deltaDone = mission.done - h.lastDone; boolean hasError = mission.errCode != ERROR_NOTHING; - // on error hide marquee or show if condition (mission.done < 1 || mission.unknownLength) is true - h.progress.setMarquee(!hasError && (mission.done < 1 || mission.unknownLength)); + // hide on error + // show if current resource length is not fetched + // show if length is unknown + h.progress.setMarquee(!hasError && (!mission.isInitialized() || mission.unknownLength)); float progress; if (mission.unknownLength) { @@ -305,36 +326,64 @@ public class MissionAdapter extends Adapter { } } - private boolean viewWithFileProvider(@NonNull File file) { - if (!file.exists()) return true; + private void viewWithFileProvider(Mission mission) { + if (checkInvalidFile(mission)) return; - String ext = Utility.getFileExt(file.getName()); - if (ext == null) return false; + String mimeType = resolveMimeType(mission); - String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1)); - Log.v(TAG, "Mime: " + mimeType + " package: " + BuildConfig.APPLICATION_ID + ".provider"); + if (BuildConfig.DEBUG) + Log.v(TAG, "Mime: " + mimeType + " package: " + BuildConfig.APPLICATION_ID + ".provider"); - Uri uri = FileProvider.getUriForFile(mContext, BuildConfig.APPLICATION_ID + ".provider", file); + Uri uri = FileProvider.getUriForFile( + mContext, + BuildConfig.APPLICATION_ID + ".provider", + mission.getDownloadedFile() + ); Intent intent = new Intent(); intent.setAction(Intent.ACTION_VIEW); intent.setDataAndType(uri, mimeType); intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { intent.addFlags(FLAG_GRANT_PREFIX_URI_PERMISSION); } - if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { intent.addFlags(FLAG_ACTIVITY_NEW_TASK); } + //mContext.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); - Log.v(TAG, "Starting intent: " + intent); + if (intent.resolveActivity(mContext.getPackageManager()) != null) { mContext.startActivity(intent); } else { - Toast noPlayerToast = Toast.makeText(mContext, R.string.toast_no_player, Toast.LENGTH_LONG); - noPlayerToast.show(); + Toast.makeText(mContext, R.string.toast_no_player, Toast.LENGTH_LONG).show(); } + } + private void shareFile(Mission mission) { + if (checkInvalidFile(mission)) return; + + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType(resolveMimeType(mission)); + intent.putExtra(Intent.EXTRA_STREAM, mission.getDownloadedFile().toURI()); + + mContext.startActivity(Intent.createChooser(intent, null)); + } + + private static String resolveMimeType(@NonNull Mission mission) { + String ext = Utility.getFileExt(mission.getDownloadedFile().getName()); + if (ext == null) return DEFAULT_MIME_TYPE; + + String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1)); + + return mimeType == null ? DEFAULT_MIME_TYPE : mimeType; + } + + private boolean checkInvalidFile(@NonNull Mission mission) { + if (mission.getDownloadedFile().exists()) return false; + + Toast.makeText(mContext, R.string.missing_file, Toast.LENGTH_SHORT).show(); return true; } @@ -343,15 +392,9 @@ public class MissionAdapter extends Adapter { } private void onServiceMessage(@NonNull Message msg) { - switch (msg.what) { - case DownloadManagerService.MESSAGE_PROGRESS: - setAutoRefresh(true); - return; - case DownloadManagerService.MESSAGE_ERROR: - case DownloadManagerService.MESSAGE_FINISHED: - break; - default: - return; + if (msg.what == DownloadManagerService.MESSAGE_PROGRESS) { + setAutoRefresh(true); + return; } for (int i = 0; i < mPendingDownloadsItems.size(); i++) { @@ -370,74 +413,98 @@ public class MissionAdapter extends Adapter { } private void showError(@NonNull DownloadMission mission) { - StringBuilder str = new StringBuilder(); - str.append(mContext.getString(R.string.label_code)); - str.append(": "); - str.append(mission.errCode); - str.append('\n'); + @StringRes int msg = R.string.general_error; + String msgEx = null; switch (mission.errCode) { case 416: - str.append(mContext.getString(R.string.error_http_requested_range_not_satisfiable)); + msg = R.string.error_http_requested_range_not_satisfiable; break; case 404: - str.append(mContext.getString(R.string.error_http_not_found)); + msg = R.string.error_http_not_found; break; case ERROR_NOTHING: - str.append("¿?"); - break; + return;// this never should happen case ERROR_FILE_CREATION: - str.append(mContext.getString(R.string.error_file_creation)); + msg = R.string.error_file_creation; break; case ERROR_HTTP_NO_CONTENT: - str.append(mContext.getString(R.string.error_http_no_content)); + msg = R.string.error_http_no_content; break; case ERROR_HTTP_UNSUPPORTED_RANGE: - str.append(mContext.getString(R.string.error_http_unsupported_range)); + msg = R.string.error_http_unsupported_range; break; case ERROR_PATH_CREATION: - str.append(mContext.getString(R.string.error_path_creation)); + msg = R.string.error_path_creation; break; case ERROR_PERMISSION_DENIED: - str.append(mContext.getString(R.string.permission_denied)); + msg = R.string.permission_denied; break; case ERROR_SSL_EXCEPTION: - str.append(mContext.getString(R.string.error_ssl_exception)); + msg = R.string.error_ssl_exception; break; case ERROR_UNKNOWN_HOST: - str.append(mContext.getString(R.string.error_unknown_host)); + msg = R.string.error_unknown_host; break; case ERROR_CONNECT_HOST: - str.append(mContext.getString(R.string.error_connect_host)); + msg = R.string.error_connect_host; + break; + case ERROR_POSTPROCESSING_STOPPED: + msg = R.string.error_postprocessing_stopped; break; case ERROR_POSTPROCESSING: - str.append(mContext.getString(R.string.error_postprocessing_failed)); - case ERROR_UNKNOWN_EXCEPTION: + case ERROR_POSTPROCESSING_HOLD: + showError(mission.errObject, UserAction.DOWNLOAD_POSTPROCESSING, R.string.error_postprocessing_failed); + return; + case ERROR_INSUFFICIENT_STORAGE: + msg = R.string.error_insufficient_storage; break; + case ERROR_UNKNOWN_EXCEPTION: + showError(mission.errObject, UserAction.DOWNLOAD_FAILED, R.string.general_error); + return; default: if (mission.errCode >= 100 && mission.errCode < 600) { - str = new StringBuilder(8); - str.append("HTTP "); - str.append(mission.errCode); + msgEx = "HTTP " + mission.errCode; } else if (mission.errObject == null) { - str.append("(not_decelerated_error_code)"); + msgEx = "(not_decelerated_error_code)"; + } else { + showError(mission.errObject, UserAction.DOWNLOAD_FAILED, msg); + return; } break; } - if (mission.errObject != null) { - str.append("\n\n"); - str.append(mission.errObject.toString()); + AlertDialog.Builder builder = new AlertDialog.Builder(mContext); + + if (msgEx != null) + builder.setMessage(msgEx); + else + builder.setMessage(msg); + + // add report button for non-HTTP errors (range 100-599) + if (mission.errObject != null && (mission.errCode < 100 || mission.errCode >= 600)) { + @StringRes final int mMsg = msg; + builder.setPositiveButton(R.string.error_report_title, (dialog, which) -> + showError(mission.errObject, UserAction.DOWNLOAD_FAILED, mMsg) + ); } - AlertDialog.Builder builder = new AlertDialog.Builder(mContext); - builder.setTitle(mission.name) - .setMessage(str) - .setNegativeButton(android.R.string.ok, (dialog, which) -> dialog.cancel()) + builder.setNegativeButton(android.R.string.ok, (dialog, which) -> dialog.cancel()) + .setTitle(mission.name) .create() .show(); } + private void showError(Exception exception, UserAction action, @StringRes int reason) { + ErrorActivity.reportError( + mContext, + Collections.singletonList(exception), + null, + null, + ErrorActivity.ErrorInfo.make(action, "-", "-", reason) + ); + } + public void clearFinishedDownloads() { mDownloadManager.forgetFinishedDownloads(); applyChanges(); @@ -466,16 +533,24 @@ public class MissionAdapter extends Adapter { showError(mission); return true; case R.id.queue: - h.queue.setChecked(!h.queue.isChecked()); - mission.enqueued = h.queue.isChecked(); + boolean flag = !h.queue.isChecked(); + h.queue.setChecked(flag); + mission.setEnqueued(flag); updateProgress(h); return true; + case R.id.retry: + mission.psContinue(true); + return true; + case R.id.cancel: + mission.psContinue(false); + return false; } } switch (id) { - case R.id.open: - return viewWithFileProvider(h.item.mission.getDownloadedFile()); + case R.id.menu_item_share: + shareFile(h.item.mission); + return true; case R.id.delete: if (mDeleter == null) { mDownloadManager.deleteMission(h.item.mission); @@ -529,15 +604,42 @@ public class MissionAdapter extends Adapter { } public void setClearButton(MenuItem clearButton) { - if (mClear == null) clearButton.setVisible(mIterator.hasFinishedMissions()); + if (mClear == null) + clearButton.setVisible(mIterator.hasFinishedMissions()); + mClear = clearButton; } + public void setMasterButtons(MenuItem startButton, MenuItem pauseButton) { + boolean init = mStartButton == null || mPauseButton == null; + + mStartButton = startButton; + mPauseButton = pauseButton; + + if (init) checkMasterButtonsVisibility(); + } + private void checkEmptyMessageVisibility() { int flag = mIterator.getOldListSize() > 0 ? View.GONE : View.VISIBLE; if (mEmptyMessage.getVisibility() != flag) mEmptyMessage.setVisibility(flag); } + private void checkMasterButtonsVisibility() { + boolean[] state = mIterator.hasValidPendingMissions(); + + mStartButton.setVisible(state[0]); + mPauseButton.setVisible(state[1]); + } + + public void ensurePausedMissions() { + for (ViewHolderItem h : mPendingDownloadsItems) { + if (((DownloadMission) h.item.mission).running) continue; + updateProgress(h); + h.lastTimeStamp = -1; + h.lastDone = -1; + } + } + public void deleterDispose(Bundle bundle) { if (mDeleter != null) mDeleter.dispose(bundle); @@ -604,6 +706,8 @@ public class MissionAdapter extends Adapter { ProgressDrawable progress; PopupMenu popupMenu; + MenuItem retry; + MenuItem cancel; MenuItem start; MenuItem pause; MenuItem open; @@ -636,22 +740,34 @@ public class MissionAdapter extends Adapter { button.setOnClickListener(v -> showPopupMenu()); Menu menu = popupMenu.getMenu(); + retry = menu.findItem(R.id.retry); + cancel = menu.findItem(R.id.cancel); start = menu.findItem(R.id.start); pause = menu.findItem(R.id.pause); - open = menu.findItem(R.id.open); + open = menu.findItem(R.id.menu_item_share); queue = menu.findItem(R.id.queue); showError = menu.findItem(R.id.error_message_view); delete = menu.findItem(R.id.delete); source = menu.findItem(R.id.source); checksum = menu.findItem(R.id.checksum); - itemView.setOnClickListener((v) -> { + itemView.setHapticFeedbackEnabled(true); + + itemView.setOnClickListener(v -> { if (item.mission instanceof FinishedMission) - viewWithFileProvider(item.mission.getDownloadedFile()); + viewWithFileProvider(item.mission); + }); + + itemView.setOnLongClickListener(v -> { + v.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + showPopupMenu(); + return true; }); } private void showPopupMenu() { + retry.setVisible(false); + cancel.setVisible(false); start.setVisible(false); pause.setVisible(false); open.setVisible(false); @@ -664,7 +780,16 @@ public class MissionAdapter extends Adapter { DownloadMission mission = item.mission instanceof DownloadMission ? (DownloadMission) item.mission : null; if (mission != null) { - if (!mission.isPsRunning()) { + if (mission.isPsRunning()) { + switch (mission.errCode) { + case ERROR_INSUFFICIENT_STORAGE: + case ERROR_POSTPROCESSING_HOLD: + retry.setVisible(true); + cancel.setVisible(true); + showError.setVisible(true); + break; + } + } else { if (mission.running) { pause.setVisible(true); } else { diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java index c4fd3b5fd..a3786a5e6 100644 --- a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java +++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java @@ -35,6 +35,8 @@ public class MissionsFragment extends Fragment { private boolean mLinear; private MenuItem mSwitch; private MenuItem mClear = null; + private MenuItem mStart = null; + private MenuItem mPause = null; private RecyclerView mList; private View mEmpty; @@ -54,9 +56,11 @@ public class MissionsFragment extends Fragment { mBinder = (DownloadManagerService.DMBinder) binder; mBinder.clearDownloadNotifications(); - mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mClear, mEmpty); + mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty); mAdapter.deleterLoad(mBundle, getView()); + setAdapterButtons(); + mBundle = null; mBinder.addMissionEventListener(mAdapter.getMessenger()); @@ -132,7 +136,7 @@ public class MissionsFragment extends Fragment { public void onAttach(Activity activity) { super.onAttach(activity); - mContext = activity.getApplicationContext(); + mContext = activity; } @@ -154,7 +158,11 @@ public class MissionsFragment extends Fragment { public void onPrepareOptionsMenu(Menu menu) { mSwitch = menu.findItem(R.id.switch_mode); mClear = menu.findItem(R.id.clear_list); - if (mAdapter != null) mAdapter.setClearButton(mClear); + mStart = menu.findItem(R.id.start_downloads); + mPause = menu.findItem(R.id.pause_downloads); + + if (mAdapter != null) setAdapterButtons(); + super.onPrepareOptionsMenu(menu); } @@ -168,6 +176,14 @@ public class MissionsFragment extends Fragment { case R.id.clear_list: mAdapter.clearFinishedDownloads(); return true; + case R.id.start_downloads: + item.setVisible(false); + mBinder.getDownloadManager().startAllMissions(); + return true; + case R.id.pause_downloads: + item.setVisible(false); + mBinder.getDownloadManager().pauseAllMissions(false); + mAdapter.ensurePausedMissions();// update items view default: return super.onOptionsItemSelected(item); } @@ -193,9 +209,9 @@ public class MissionsFragment extends Fragment { int icon; if (mLinear) - icon = isLight ? R.drawable.ic_list_black_24dp : R.drawable.ic_list_white_24dp; - else icon = isLight ? R.drawable.ic_grid_black_24dp : R.drawable.ic_grid_white_24dp; + else + icon = isLight ? R.drawable.ic_list_black_24dp : R.drawable.ic_list_white_24dp; mSwitch.setIcon(icon); mSwitch.setTitle(mLinear ? R.string.grid : R.string.list); @@ -203,6 +219,13 @@ public class MissionsFragment extends Fragment { } } + private void setAdapterButtons() { + if (mClear == null || mStart == null || mPause == null) return; + + mAdapter.setClearButton(mClear); + mAdapter.setMasterButtons(mStart, mPause); + } + @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); diff --git a/app/src/main/res/drawable-hdpi/ic_pause_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_pause_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..3770b9124c41ecdf986c9f15ebc9d33f4d308449 GIT binary patch literal 135 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbB{5)M8Ln2z=PBY|UFyLUW-FBh< z^_6eTubwCrvELNhVW`Bwps3y|k1|%Oc%$NbB0zF+ELn2z=UfamYV8Fq8u}3$h zU~LlHiQ}i74$0oo+;eGbN5?&*;_i(}_pkV>zv%dwbpNWax`*h|x)sb04GfG-EF1y~ m4)V>%_HNibuSV(DN^Z6)zQ%0|Gu8tQXYh3Ob6Mw<&;$UCzc1YY literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_pause_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_pause_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..6e81d3ad4f2029f7a521349688dff470b5858019 GIT binary patch literal 109 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjCY~;iAs(H{2@4 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_pause_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_pause_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..f5236e8aa89c24a1f42dbd298dbb08b871a05722 GIT binary patch literal 112 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`Gj=AJH&As(H{2@lo1`?|x>U_dm*KlFIf!UpaSA_<7IjIM7lCPgg&ebxsLQ02LECbpQYW literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_pause_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_pause_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..b94b2ae40da8a50607f4dbb92efc57ea7fac9e4d GIT binary patch literal 139 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpUtAWs*^kc`H+Hx#)T6a-uy-`C&l zJ+?I>mRE`6^MMtAR&G=CoMfkETwVC+L0w4x9)^%y_A?7l12v+9F2?_Lx?Yo(bn_}W Vew)4B;Nvfl3Qt!*mvv4FO#oAVDPsTt literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_pause_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_pause_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..2691adeb30e1ada4ed84340315848782199c6ceb GIT binary patch literal 196 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!-JULvAr*7p-rC4}K!Jzh;G6yG z+#4K>9KRTe9-aF{>~#ER8=xwNKR22q+25~!*!Ox*v!!JMe|e1i0)+qp4Gs|&E+*E- jCI?6Ix&2Q-Hhllhz@PfGi~DL_K8Wk->gTe~DWM4fwX--o literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_pause_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_pause_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..15cb0b51ce27438dd5cfa97a2a8f84c456647ec5 GIT binary patch literal 206 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!Q$1ZALn`LHy=BOC*g%BU@&5mL zyBzdBT;9R6M)dI^1LiE%d)KV^fC?GvX3ji+EVBO2{<3=&Nso0mr&k}a{If~j?4DA9 wfCh&M3l|e>W0Qm90tK?Ui8U(hK#MAx_0^?TpQBfB_G~#&`Cq zW+yn_@)bVN@OZG@_uc&Gj6hXD&`?}$@cPg{(>)Kr-#d7xqQMr(VCmo#5LQrg@M)OA vV91!nbcDtNH=dk6QJVN)>K?>N3=HmeHaFkJH)U`C2NLmg^>bP0l+XkKR3S{3 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_pause_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_pause_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..12a49bc12b8a00c5acdbcac690d1a3cc266c466f GIT binary patch literal 254 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoM?GB}Ln`LHy>pQBh=Bl$<8%3? z#swVJ^6n2rP1r*hY(MfP{yHmA9SF#uJ+s;3d*ko-XQlakF3g(ymErw_`Eze5s5 + + + + + + + + + @@ -13,8 +22,8 @@ android:checkable="true"/> + android:id="@+id/menu_item_share" + android:title="@string/share" /> 系统拒绝该行动 下载失败 下载完成 - %已下载完毕 + %s已下载完毕 生成独特的名字 覆写 同名的已下载文件已经存在 diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index e75d905df..818efc74c 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -159,7 +159,7 @@ abrir en modo popup Si tienes ideas de; traducción, cambios de diseño, limpieza de código o cambios de código realmente fuertes—la ayuda siempre es bienvenida. Cuanto más se hace, mejor se pone! Leer licencia Contribuir - Suscribirse +Suscribirse Suscrito Canal no suscrito No se pudo cambiar la suscripción @@ -211,8 +211,8 @@ abrir en modo popup Vídeos Elemento eliminado - ¿Desea eliminar este elemento del historial de búsqueda? - Contenido de la página principal +¿Desea eliminar este elemento del historial de búsqueda? +Contenido de la página principal Página en blanco Página del kiosco Página de suscripción @@ -224,7 +224,7 @@ abrir en modo popup Kiosco Tendencias Top 50 - Mostrar sugerencia cuando se presiona el botón de segundo plano o popup en la página de detalles del vídeo +Mostrar sugerencia cuando se presiona el botón de segundo plano o popup en la página de detalles del vídeo En cola en el reproductor de fondo En cola en el reproductor popup Reproducir todo @@ -242,7 +242,7 @@ abrir en modo popup Comenzar a reproducir aquí Comenzar aquí en segundo plano Comenzar aquí en popup - Mostrar consejo \"Mantener para poner en la cola\" +Mostrar consejo \"Mantener para poner en la cola\" Nuevo y popular Mantener para poner en la cola Donar @@ -270,7 +270,7 @@ abrir en modo popup Reproductor de popup Obteniendo información… Cargando contenido solicitado - Importar base de datos +Importar base de datos Exportar base de datos Reemplazará su historial actual y sus suscripciones Exportar historial, suscripciones y listas de reproducción @@ -325,6 +325,7 @@ abrir en modo popup DIRECTO SINCRONIZAR Archivo + Archivo movido o eliminado No existe el directorio No existe la fuente del archivo/contenido El archivo no existe o insuficientes permisos para leerlo o escribir en él @@ -419,6 +420,8 @@ abrir en modo popup Sobrescribir Ya existe un archivo descargado con este nombre Hay una descarga en curso con este nombre + Hay una descarga pendiente con este nombre + Mostrar como grilla Mostrar como lista Limpiar descargas finalizadas @@ -426,8 +429,14 @@ abrir en modo popup Detener Intentos máximos Cantidad máxima de intentos antes de cancelar la descarga - Pausar al cambiar a datos moviles - Las descargas que no se pueden pausar serán reiniciadas + Interrumpir en redes medidas + Útil al cambiar a Datos Móviles, solo algunas descargas no se pueden suspender + Limitar cola de descarga + Solo se permitirá una descarga a la vez + Iniciar descargas + Pausar descargas + + Mostrar error Codigo @@ -439,9 +448,12 @@ abrir en modo popup No se puede conectar con el servidor El servidor no devolvio datos El servidor no acepta descargas multi-hilos, intente de nuevo con @string/msg_threads = 1 - El rango solicitado no se puede satisfacer + No se logro obtener el rango solicitado No encontrado Fallo el post-procesado + NewPipe se cerro mientras se trabajaba en el archivo + No hay suficiente espacio disponible en el dispositivo + Desuscribirse Nueva pestaña Elige la pestaña diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 865b68c24..37bc9eec6 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -25,6 +25,7 @@ + diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 3861f53d5..214a074c4 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -192,6 +192,7 @@ cross_network_downloads + downloads_queue_limit default_download_threads diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 154b8e0c4..afc6afeb3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -196,6 +196,7 @@ Invalid URL No video streams found No audio streams found + File moved or deleted No such folder No such file/content source The file doesn\'t exist or permission to read or write to it is lacking @@ -513,6 +514,8 @@ Overwrite A downloaded file with this name already exists There is a download in progress with this name + There is a pending download with this name + Show error Code @@ -527,13 +530,20 @@ Requested range not satisfiable Not found Post-processing failed + NewPipe was closed while working on the file + No space left on device + Clear finished downloads Continue your %s pending transfers from Downloads Stop Maximum retries Maximum number of attempts before canceling the download - Pause on switching to mobile data - Downloads that can not be paused will be restarted + Interrupt on metered networks + Useful when switching to mobile data, although some downloads cannot be suspended Close + Limit download queue + One download will run at the same time + Start downloads + Pause downloads \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index a7686dedc..51043718a 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -41,6 +41,7 @@ @drawable/ic_arrow_top_left_black_24dp @drawable/ic_more_vert_black_24dp @drawable/ic_play_arrow_black_24dp + @drawable/ic_pause_black_24dp @drawable/ic_settings_black_24dp @drawable/ic_whatshot_black_24dp @drawable/ic_channel_black_24dp @@ -119,6 +120,7 @@ @drawable/ic_list_white_24dp @drawable/ic_grid_white_24dp @drawable/ic_delete_white_24dp + @drawable/ic_pause_white_24dp @drawable/ic_settings_update_white @color/dark_separator_color diff --git a/app/src/main/res/xml/download_settings.xml b/app/src/main/res/xml/download_settings.xml index be015018a..9f32e7f2f 100644 --- a/app/src/main/res/xml/download_settings.xml +++ b/app/src/main/res/xml/download_settings.xml @@ -50,4 +50,11 @@ android:summary="@string/pause_downloads_on_mobile_desc" android:title="@string/pause_downloads_on_mobile" /> + + From f6b32823ba525c62241c65ba2327bc10a9cc107a Mon Sep 17 00:00:00 2001 From: kapodamy Date: Fri, 5 Apr 2019 14:45:39 -0300 Subject: [PATCH 128/138] Implement Storage Access Framework * re-work finished mission database * re-work DownloadMission and bump it Serializable version * keep the classic Java IO API * SAF Tree API support on Android Lollipop or higher * add wrapper for SAF stream opening * implement Closeable in SharpStream to replace the dispose() method * do required changes for this API: ** remove any file creation logic from DownloadInitializer ** make PostProcessing Serializable and reduce the number of iterations ** update all strings.xml files ** storage helpers: StoredDirectoryHelper & StoredFileHelper ** best effort to handle any kind of SAF errors/exceptions --- .../newpipe/download/DownloadActivity.java | 2 +- .../newpipe/download/DownloadDialog.java | 414 +++++++++++++----- .../newpipe/local/feed/FeedFragment.java | 4 +- .../player/playback/MediaSourceManager.java | 2 +- .../settings/DownloadSettingsFragment.java | 201 +++++++-- .../newpipe/settings/NewPipeSettings.java | 35 +- .../newpipe/streams/Mp4FromDashWriter.java | 2 +- .../schabi/newpipe/streams/WebMWriter.java | 2 +- .../newpipe/streams/io/SharpStream.java | 12 +- .../giga/get/DownloadInitializer.java | 36 +- .../us/shandian/giga/get/DownloadMission.java | 132 +++--- .../shandian/giga/get/DownloadRunnable.java | 11 +- .../giga/get/DownloadRunnableFallback.java | 16 +- .../us/shandian/giga/get/FinishedMission.java | 6 +- .../java/us/shandian/giga/get/Mission.java | 35 +- .../giga/get/sqlite/DownloadDataSource.java | 73 --- .../get/sqlite/DownloadMissionHelper.java | 112 ----- .../giga/get/sqlite/FinishedMissionStore.java | 223 ++++++++++ .../io/ChunkFileInputStream.java | 298 +++++++------ .../io/CircularFileWriter.java | 54 +-- .../java/us/shandian/giga/io/FileStream.java | 131 ++++++ .../us/shandian/giga/io/FileStreamSAF.java | 140 ++++++ .../io/SharpInputStream.java | 122 +++--- .../giga/io/StoredDirectoryHelper.java | 175 ++++++++ .../us/shandian/giga/io/StoredFileHelper.java | 301 +++++++++++++ .../giga/postprocessing/M4aNoDash.java | 6 +- .../giga/postprocessing/Mp4FromDashMuxer.java | 6 +- .../giga/postprocessing/Postprocessing.java | 108 +++-- .../giga/postprocessing/TtmlConverter.java | 9 +- .../giga/postprocessing/WebMMuxer.java | 6 +- .../giga/service/DownloadManager.java | 298 ++++++------- .../giga/service/DownloadManagerService.java | 204 ++++++--- .../shandian/giga/service/MissionState.java | 5 + .../giga/ui/adapter/MissionAdapter.java | 97 +++- .../us/shandian/giga/ui/common/Deleter.java | 52 +-- .../giga/ui/fragment/MissionsFragment.java | 57 ++- .../java/us/shandian/giga/util/Utility.java | 23 +- app/src/main/res/values-ar/strings.xml | 6 +- app/src/main/res/values-ca/strings.xml | 4 +- app/src/main/res/values-cmn/strings.xml | 6 +- app/src/main/res/values-da/strings.xml | 4 +- app/src/main/res/values-de/strings.xml | 6 +- app/src/main/res/values-es/strings.xml | 25 +- app/src/main/res/values-eu/strings.xml | 6 +- app/src/main/res/values-he/strings.xml | 6 +- app/src/main/res/values-id/strings.xml | 6 +- app/src/main/res/values-it/strings.xml | 6 +- app/src/main/res/values-ja/strings.xml | 12 +- app/src/main/res/values-ms/strings.xml | 6 +- app/src/main/res/values-nb-rNO/strings.xml | 6 +- app/src/main/res/values-nl-rBE/strings.xml | 6 +- app/src/main/res/values-nl/strings.xml | 6 +- app/src/main/res/values-pl/strings.xml | 6 +- app/src/main/res/values-pt-rBR/strings.xml | 6 +- app/src/main/res/values-pt/strings.xml | 6 +- app/src/main/res/values-ru/strings.xml | 6 +- app/src/main/res/values-tr/strings.xml | 6 +- app/src/main/res/values-vi/strings.xml | 6 +- app/src/main/res/values-zh-rTW/strings.xml | 6 +- app/src/main/res/values/settings_keys.xml | 17 +- app/src/main/res/values/strings.xml | 21 +- app/src/main/res/xml/download_settings.xml | 18 +- 62 files changed, 2439 insertions(+), 1180 deletions(-) delete mode 100644 app/src/main/java/us/shandian/giga/get/sqlite/DownloadDataSource.java delete mode 100644 app/src/main/java/us/shandian/giga/get/sqlite/DownloadMissionHelper.java create mode 100644 app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java rename app/src/main/java/us/shandian/giga/{postprocessing => }/io/ChunkFileInputStream.java (80%) rename app/src/main/java/us/shandian/giga/{postprocessing => }/io/CircularFileWriter.java (89%) create mode 100644 app/src/main/java/us/shandian/giga/io/FileStream.java create mode 100644 app/src/main/java/us/shandian/giga/io/FileStreamSAF.java rename app/src/main/java/us/shandian/giga/{postprocessing => }/io/SharpInputStream.java (91%) create mode 100644 app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java create mode 100644 app/src/main/java/us/shandian/giga/io/StoredFileHelper.java create mode 100644 app/src/main/java/us/shandian/giga/service/MissionState.java diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java index 7ee686a66..41971dfd4 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java @@ -55,7 +55,7 @@ public class DownloadActivity extends AppCompatActivity { private void updateFragments() { MissionsFragment fragment = new MissionsFragment(); - getFragmentManager().beginTransaction() + getSupportFragmentManager().beginTransaction() .replace(R.id.frame, fragment, MISSIONS_FRAGMENT_TAG) .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) .commit(); diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index 0b4767133..4525c5988 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -1,8 +1,14 @@ package org.schabi.newpipe.download; +import android.app.Activity; +import android.content.ComponentName; import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; import android.content.SharedPreferences; +import android.net.Uri; import android.os.Bundle; +import android.os.IBinder; import android.preference.PreferenceManager; import android.support.annotation.IdRes; import android.support.annotation.NonNull; @@ -14,6 +20,7 @@ import android.support.v7.widget.Toolbar; import android.util.Log; import android.util.SparseArray; import android.view.LayoutInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; @@ -35,7 +42,8 @@ import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.SubtitlesStream; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.extractor.utils.Localization; -import org.schabi.newpipe.settings.NewPipeSettings; +import org.schabi.newpipe.report.ErrorActivity; +import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.FilenameUtils; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.PermissionHelper; @@ -44,20 +52,27 @@ import org.schabi.newpipe.util.StreamItemAdapter; import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper; import org.schabi.newpipe.util.ThemeHelper; +import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Locale; import icepick.Icepick; import icepick.State; import io.reactivex.disposables.CompositeDisposable; +import us.shandian.giga.io.StoredDirectoryHelper; +import us.shandian.giga.io.StoredFileHelper; import us.shandian.giga.postprocessing.Postprocessing; +import us.shandian.giga.service.DownloadManager; import us.shandian.giga.service.DownloadManagerService; -import us.shandian.giga.service.DownloadManagerService.MissionCheck; +import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder; +import us.shandian.giga.service.MissionState; public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener { private static final String TAG = "DialogFragment"; private static final boolean DEBUG = MainActivity.DEBUG; + private static final int REQUEST_DOWNLOAD_PATH_SAF = 0x1230; @State protected StreamInfo currentInfo; @@ -82,7 +97,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck private EditText nameEditText; private Spinner streamsSpinner; - private RadioGroup radioVideoAudioGroup; + private RadioGroup radioStreamsGroup; private TextView threadsCountTextView; private SeekBar threadsSeekBar; @@ -162,7 +177,11 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck return; } - setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(getContext())); + final Context context = getContext(); + if (context == null) + throw new RuntimeException("Context was null"); + + setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context)); Icepick.restoreInstanceState(this, savedInstanceState); SparseArray> secondaryStreams = new SparseArray<>(4); @@ -179,9 +198,32 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck } } - this.videoStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedVideoStreams, secondaryStreams); - this.audioStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedAudioStreams); - this.subtitleStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedSubtitleStreams); + this.videoStreamsAdapter = new StreamItemAdapter<>(context, wrappedVideoStreams, secondaryStreams); + this.audioStreamsAdapter = new StreamItemAdapter<>(context, wrappedAudioStreams); + this.subtitleStreamsAdapter = new StreamItemAdapter<>(context, wrappedSubtitleStreams); + + Intent intent = new Intent(context, DownloadManagerService.class); + context.startService(intent); + + context.bindService(intent, new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName cname, IBinder service) { + DownloadManagerBinder mgr = (DownloadManagerBinder) service; + + mainStorageAudio = mgr.getMainStorageAudio(); + mainStorageVideo = mgr.getMainStorageVideo(); + downloadManager = mgr.getDownloadManager(); + + okButton.setEnabled(true); + + context.unbindService(this); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + // nothing to do + } + }, Context.BIND_AUTO_CREATE); } @Override @@ -206,8 +248,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck threadsCountTextView = view.findViewById(R.id.threads_count); threadsSeekBar = view.findViewById(R.id.threads); - radioVideoAudioGroup = view.findViewById(R.id.video_audio_group); - radioVideoAudioGroup.setOnCheckedChangeListener(this); + radioStreamsGroup = view.findViewById(R.id.video_audio_group); + radioStreamsGroup.setOnCheckedChangeListener(this); initToolbar(view.findViewById(R.id.toolbar)); setupDownloadOptions(); @@ -242,17 +284,17 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck disposables.clear(); disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams).subscribe(result -> { - if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.video_button) { + if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.video_button) { setupVideoSpinner(); } })); disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams).subscribe(result -> { - if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) { + if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.audio_button) { setupAudioSpinner(); } })); disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams).subscribe(result -> { - if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) { + if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.subtitle_button) { setupSubtitleSpinner(); } })); @@ -270,17 +312,40 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck Icepick.saveInstanceState(this, outState); } + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (requestCode == REQUEST_DOWNLOAD_PATH_SAF && resultCode == Activity.RESULT_OK) { + if (data.getData() == null) { + showFailedDialog(R.string.general_error); + return; + } + try { + continueSelectedDownload(new StoredFileHelper(getContext(), data.getData(), "")); + } catch (IOException e) { + showErrorActivity(e); + } + } + } + /*////////////////////////////////////////////////////////////////////////// // Inits //////////////////////////////////////////////////////////////////////////*/ private void initToolbar(Toolbar toolbar) { if (DEBUG) Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]"); + + boolean isLight = ThemeHelper.isLightThemeSelected(getActivity()); + okButton = toolbar.findViewById(R.id.okay); + okButton.setEnabled(false);// disable until the download service connection is done + toolbar.setTitle(R.string.download_dialog_title); - toolbar.setNavigationIcon(ThemeHelper.isLightThemeSelected(getActivity()) ? R.drawable.ic_arrow_back_black_24dp : R.drawable.ic_arrow_back_white_24dp); + toolbar.setNavigationIcon(isLight ? R.drawable.ic_arrow_back_black_24dp : R.drawable.ic_arrow_back_white_24dp); toolbar.inflateMenu(R.menu.dialog_url); toolbar.setNavigationOnClickListener(v -> getDialog().dismiss()); + toolbar.setOnMenuItemClickListener(item -> { if (item.getItemId() == R.id.okay) { prepareSelectedDownload(); @@ -348,7 +413,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck public void onItemSelected(AdapterView parent, View view, int position, long id) { if (DEBUG) Log.d(TAG, "onItemSelected() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]"); - switch (radioVideoAudioGroup.getCheckedRadioButtonId()) { + switch (radioStreamsGroup.getCheckedRadioButtonId()) { case R.id.audio_button: selectedAudioIndex = position; break; @@ -372,9 +437,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck protected void setupDownloadOptions() { setRadioButtonsState(false); - final RadioButton audioButton = radioVideoAudioGroup.findViewById(R.id.audio_button); - final RadioButton videoButton = radioVideoAudioGroup.findViewById(R.id.video_button); - final RadioButton subtitleButton = radioVideoAudioGroup.findViewById(R.id.subtitle_button); + final RadioButton audioButton = radioStreamsGroup.findViewById(R.id.audio_button); + final RadioButton videoButton = radioStreamsGroup.findViewById(R.id.video_button); + final RadioButton subtitleButton = radioStreamsGroup.findViewById(R.id.subtitle_button); final boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0; final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0; final boolean isSubtitleStreamsAvailable = subtitleStreamsAdapter.getCount() > 0; @@ -399,9 +464,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck } private void setRadioButtonsState(boolean enabled) { - radioVideoAudioGroup.findViewById(R.id.audio_button).setEnabled(enabled); - radioVideoAudioGroup.findViewById(R.id.video_button).setEnabled(enabled); - radioVideoAudioGroup.findViewById(R.id.subtitle_button).setEnabled(enabled); + radioStreamsGroup.findViewById(R.id.audio_button).setEnabled(enabled); + radioStreamsGroup.findViewById(R.id.video_button).setEnabled(enabled); + radioStreamsGroup.findViewById(R.id.subtitle_button).setEnabled(enabled); } private int getSubtitleIndexBy(List streams) { @@ -436,119 +501,248 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck return 0; } + StoredDirectoryHelper mainStorageAudio = null; + StoredDirectoryHelper mainStorageVideo = null; + DownloadManager downloadManager = null; + + MenuItem okButton = null; + + private String getNameEditText() { + return nameEditText.getText().toString().trim(); + } + + private void showFailedDialog(@StringRes int msg) { + new AlertDialog.Builder(getContext()) + .setMessage(msg) + .setNegativeButton(android.R.string.ok, null) + .create() + .show(); + } + + private void showErrorActivity(Exception e) { + ErrorActivity.reportError( + getContext(), + Collections.singletonList(e), + null, + null, + ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "-", "-", R.string.general_error) + ); + } + private void prepareSelectedDownload() { final Context context = getContext(); - Stream stream; - String location; - char kind; + StoredDirectoryHelper mainStorage; + MediaFormat format; + String mime; - String fileName = nameEditText.getText().toString().trim(); - if (fileName.isEmpty()) - fileName = FilenameUtils.createFilename(context, currentInfo.getName()); + // first, build the filename and get the output folder (if possible) - switch (radioVideoAudioGroup.getCheckedRadioButtonId()) { + String filename = getNameEditText() + "."; + if (filename.isEmpty()) { + filename = FilenameUtils.createFilename(context, currentInfo.getName()); + } + filename += "."; + + switch (radioStreamsGroup.getCheckedRadioButtonId()) { case R.id.audio_button: - stream = audioStreamsAdapter.getItem(selectedAudioIndex); - location = NewPipeSettings.getAudioDownloadPath(context); - kind = 'a'; + mainStorage = mainStorageAudio; + format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat(); + mime = format.mimeType; + filename += format.suffix; break; case R.id.video_button: - stream = videoStreamsAdapter.getItem(selectedVideoIndex); - location = NewPipeSettings.getVideoDownloadPath(context); - kind = 'v'; + mainStorage = mainStorageVideo; + format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat(); + mime = format.mimeType; + filename += format.suffix; break; case R.id.subtitle_button: - stream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex); - location = NewPipeSettings.getVideoDownloadPath(context);// assume that subtitle & video files go together - kind = 's'; + mainStorage = mainStorageVideo;// subtitle & video files go together + format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat(); + mime = format.mimeType; + filename += format == MediaFormat.TTML ? MediaFormat.SRT.suffix : format.suffix; + break; + default: + throw new RuntimeException("No stream selected"); + } + + if (mainStorage == null) { + // this part is called if... + // older android version running with SAF preferred + // save path not defined (via download settings) + + StoredFileHelper.requestSafWithFileCreation(this, REQUEST_DOWNLOAD_PATH_SAF, filename, mime); + return; + } + + // check for existing file with the same name + Uri result = mainStorage.findFile(filename); + + if (result == null) { + // the file does not exists, create + StoredFileHelper storage = mainStorage.createFile(filename, mime); + if (storage == null || !storage.canWrite()) { + showFailedDialog(R.string.error_file_creation); + return; + } + + continueSelectedDownload(storage); + return; + } + + // the target filename is already use, try load + StoredFileHelper storage; + try { + storage = new StoredFileHelper(context, result, mime); + } catch (IOException e) { + showErrorActivity(e); + return; + } + + // check if is our file + MissionState state = downloadManager.checkForExistingMission(storage); + @StringRes int msgBtn; + @StringRes int msgBody; + + switch (state) { + case Finished: + msgBtn = R.string.overwrite; + msgBody = R.string.overwrite_finished_warning; + break; + case Pending: + msgBtn = R.string.overwrite; + msgBody = R.string.download_already_pending; + break; + case PendingRunning: + msgBtn = R.string.generate_unique_name; + msgBody = R.string.download_already_running; + break; + case None: + msgBtn = R.string.overwrite; + msgBody = R.string.overwrite_unrelated_warning; break; default: return; } - int threads; + // handle user answer (overwrite or create another file with different name) + final String finalFilename = filename; + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.download_dialog_title) + .setMessage(msgBody) + .setPositiveButton(msgBtn, (dialog, which) -> { + dialog.dismiss(); - if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.subtitle_button) { - threads = 1;// use unique thread for subtitles due small file size - fileName += ".srt";// final subtitle format - } else { - threads = threadsSeekBar.getProgress() + 1; - fileName += "." + stream.getFormat().getSuffix(); - } - - final String finalFileName = fileName; - - DownloadManagerService.checkForRunningMission(context, location, fileName, (MissionCheck result) -> { - @StringRes int msgBtn; - @StringRes int msgBody; - - switch (result) { - case Finished: - msgBtn = R.string.overwrite; - msgBody = R.string.overwrite_warning; - break; - case Pending: - msgBtn = R.string.overwrite; - msgBody = R.string.download_already_pending; - break; - case PendingRunning: - msgBtn = R.string.generate_unique_name; - msgBody = R.string.download_already_running; - break; - default: - downloadSelected(context, stream, location, finalFileName, kind, threads); - return; - } - - // overwrite or unique name actions are done by the download manager - - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(R.string.download_dialog_title) - .setMessage(msgBody) - .setPositiveButton( - msgBtn, - (dialog, which) -> downloadSelected(context, stream, location, finalFileName, kind, threads) - ) - .setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel()) - .create() - .show(); - }); + StoredFileHelper storageNew; + switch (state) { + case Finished: + case Pending: + downloadManager.forgetMission(storage); + case None: + // try take (or steal) the file permissions + try { + storageNew = new StoredFileHelper(context, result, mainStorage.getTag()); + if (storageNew.canWrite()) + continueSelectedDownload(storageNew); + else + showFailedDialog(R.string.error_file_creation); + } catch (IOException e) { + showErrorActivity(e); + } + break; + case PendingRunning: + // FIXME: createUniqueFile() is not tested properly + storageNew = mainStorage.createUniqueFile(finalFilename, mime); + if (storageNew == null) + showFailedDialog(R.string.error_file_creation); + else + continueSelectedDownload(storageNew); + break; + } + }) + .setNegativeButton(android.R.string.cancel, null) + .create() + .show(); } - private void downloadSelected(Context context, Stream selectedStream, String location, String fileName, char kind, int threads) { + private void continueSelectedDownload(@NonNull StoredFileHelper storage) { + final Context context = getContext(); + + if (!storage.canWrite()) { + showFailedDialog(R.string.permission_denied); + return; + } + + // check if the selected file has to be overwritten, by simply checking its length + try { + if (storage.length() > 0) storage.truncate(); + } catch (IOException e) { + Log.e(TAG, "failed to overwrite the file: " + storage.getUri().toString(), e); + //showErrorActivity(e); + showFailedDialog(R.string.overwrite_failed); + return; + } + + Stream selectedStream; + char kind; + int threads = threadsSeekBar.getProgress() + 1; String[] urls; String psName = null; String[] psArgs = null; String secondaryStreamUrl = null; long nearLength = 0; - if (selectedStream instanceof AudioStream) { - if (selectedStream.getFormat() == MediaFormat.M4A) { - psName = Postprocessing.ALGORITHM_M4A_NO_DASH; - } - } else if (selectedStream instanceof VideoStream) { - SecondaryStreamHelper secondaryStream = videoStreamsAdapter - .getAllSecondary() - .get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream)); + // more download logic: select muxer, subtitle converter, etc. + switch (radioStreamsGroup.getCheckedRadioButtonId()) { + case R.id.audio_button: + threads = 1;// use unique thread for subtitles due small file size + kind = 'a'; + selectedStream = audioStreamsAdapter.getItem(selectedAudioIndex); - if (secondaryStream != null) { - secondaryStreamUrl = secondaryStream.getStream().getUrl(); - psName = selectedStream.getFormat() == MediaFormat.MPEG_4 ? Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER : Postprocessing.ALGORITHM_WEBM_MUXER; - psArgs = null; - long videoSize = wrappedVideoStreams.getSizeInBytes((VideoStream) selectedStream); - - // set nearLength, only, if both sizes are fetched or known. this probably does not work on slow networks - if (secondaryStream.getSizeInBytes() > 0 && videoSize > 0) { - nearLength = secondaryStream.getSizeInBytes() + videoSize; + if (selectedStream.getFormat() == MediaFormat.M4A) { + psName = Postprocessing.ALGORITHM_M4A_NO_DASH; } - } - } else if ((selectedStream instanceof SubtitlesStream) && selectedStream.getFormat() == MediaFormat.TTML) { - psName = Postprocessing.ALGORITHM_TTML_CONVERTER; - psArgs = new String[]{ - selectedStream.getFormat().getSuffix(), - "false",// ignore empty frames - "false",// detect youtube duplicate lines - }; + break; + case R.id.video_button: + kind = 'v'; + selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex); + + SecondaryStreamHelper secondaryStream = videoStreamsAdapter + .getAllSecondary() + .get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream)); + + if (secondaryStream != null) { + secondaryStreamUrl = secondaryStream.getStream().getUrl(); + + if (selectedStream.getFormat() == MediaFormat.MPEG_4) + psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER; + else + psName = Postprocessing.ALGORITHM_WEBM_MUXER; + + psArgs = null; + long videoSize = wrappedVideoStreams.getSizeInBytes((VideoStream) selectedStream); + + // set nearLength, only, if both sizes are fetched or known. this probably does not work on slow networks + if (secondaryStream.getSizeInBytes() > 0 && videoSize > 0) { + nearLength = secondaryStream.getSizeInBytes() + videoSize; + } + } + break; + case R.id.subtitle_button: + kind = 's'; + selectedStream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex); + + if (selectedStream.getFormat() == MediaFormat.TTML) { + psName = Postprocessing.ALGORITHM_TTML_CONVERTER; + psArgs = new String[]{ + selectedStream.getFormat().getSuffix(), + "false",// ignore empty frames + "false",// detect youtube duplicate lines + }; + } + break; + default: + return; } if (secondaryStreamUrl == null) { @@ -557,8 +751,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck urls = new String[]{selectedStream.getUrl(), secondaryStreamUrl}; } - DownloadManagerService.startMission(context, urls, location, fileName, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength); + DownloadManagerService.startMission(context, urls, storage, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength); - getDialog().dismiss(); + dismiss(); } } diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.java b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.java index f1bb01734..475627c08 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.java @@ -21,8 +21,8 @@ import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.fragments.list.BaseListFragment; -import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.local.subscription.SubscriptionService; +import org.schabi.newpipe.report.UserAction; import java.util.Collections; import java.util.HashSet; @@ -262,7 +262,7 @@ public class FeedFragment extends BaseListFragment, Voi * If chosen feed already displayed, then we request another feed from another * subscription, until the subscription table runs out of new items. *

- * This Observer is self-contained and will dispose itself when complete. However, this + * This Observer is self-contained and will close itself when complete. However, this * does not obey the fragment lifecycle and may continue running in the background * until it is complete. This is done due to RxJava2 no longer propagate errors once * an observer is unsubscribed while the thread process is still running. diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java index db8cc797e..fb1a609cc 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java @@ -158,7 +158,7 @@ public class MediaSourceManager { * Dispose the manager and releases all message buses and loaders. * */ public void dispose() { - if (DEBUG) Log.d(TAG, "dispose() called."); + if (DEBUG) Log.d(TAG, "close() called."); debouncedSignal.onComplete(); debouncedLoader.dispose(); diff --git a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java index 82c6853d5..3737d1c17 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java @@ -2,26 +2,42 @@ package org.schabi.newpipe.settings; import android.app.Activity; import android.app.AlertDialog; +import android.content.Context; import android.content.Intent; +import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.support.annotation.Nullable; +import android.support.annotation.RequiresApi; +import android.support.annotation.StringRes; import android.support.v7.preference.Preference; import android.util.Log; - -import com.nononsenseapps.filepicker.Utils; +import android.widget.Toast; import org.schabi.newpipe.R; import org.schabi.newpipe.util.FilePickerActivityHelper; import java.io.File; +import java.io.IOException; +import java.net.URI; + +import us.shandian.giga.io.StoredDirectoryHelper; public class DownloadSettingsFragment extends BasePreferenceFragment { - private static final int REQUEST_DOWNLOAD_PATH = 0x1235; + private static final int REQUEST_DOWNLOAD_VIDEO_PATH = 0x1235; private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236; - private String DOWNLOAD_PATH_PREFERENCE; + private String DOWNLOAD_PATH_VIDEO_PREFERENCE; private String DOWNLOAD_PATH_AUDIO_PREFERENCE; + private String DOWNLOAD_STORAGE_API; + private String DOWNLOAD_STORAGE_API_DEFAULT; + + private Preference prefPathVideo; + private Preference prefPathAudio; + + private Context ctx; + @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -33,16 +49,100 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { addPreferencesFromResource(R.xml.download_settings); + + prefPathVideo = findPreference(DOWNLOAD_PATH_VIDEO_PREFERENCE); + prefPathAudio = findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE); + + updatePathPickers(usingJavaIO()); + + findPreference(DOWNLOAD_STORAGE_API).setOnPreferenceChangeListener((preference, value) -> { + boolean javaIO = DOWNLOAD_STORAGE_API_DEFAULT.equals(value); + + if (!javaIO && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + Toast.makeText(ctx, R.string.download_pick_path, Toast.LENGTH_LONG).show(); + + // forget save paths + forgetSAFTree(DOWNLOAD_PATH_VIDEO_PREFERENCE); + forgetSAFTree(DOWNLOAD_PATH_AUDIO_PREFERENCE); + + defaultPreferences.edit() + .putString(DOWNLOAD_PATH_VIDEO_PREFERENCE, "") + .putString(DOWNLOAD_PATH_AUDIO_PREFERENCE, "") + .apply(); + + updatePreferencesSummary(); + } + + updatePathPickers(javaIO); + return true; + }); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + ctx = context; + } + + @Override + public void onDetach() { + super.onDetach(); + ctx = null; + findPreference(DOWNLOAD_STORAGE_API).setOnPreferenceChangeListener(null); } private void initKeys() { - DOWNLOAD_PATH_PREFERENCE = getString(R.string.download_path_key); + DOWNLOAD_PATH_VIDEO_PREFERENCE = getString(R.string.download_path_video_key); DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key); + DOWNLOAD_STORAGE_API = getString(R.string.downloads_storage_api); + DOWNLOAD_STORAGE_API_DEFAULT = getString(R.string.downloads_storage_api_default); } private void updatePreferencesSummary() { - findPreference(DOWNLOAD_PATH_PREFERENCE).setSummary(defaultPreferences.getString(DOWNLOAD_PATH_PREFERENCE, getString(R.string.download_path_summary))); - findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE).setSummary(defaultPreferences.getString(DOWNLOAD_PATH_AUDIO_PREFERENCE, getString(R.string.download_path_audio_summary))); + prefPathVideo.setSummary( + defaultPreferences.getString(DOWNLOAD_PATH_VIDEO_PREFERENCE, getString(R.string.download_path_summary)) + ); + prefPathAudio.setSummary( + defaultPreferences.getString(DOWNLOAD_PATH_AUDIO_PREFERENCE, getString(R.string.download_path_audio_summary)) + ); + } + + private void updatePathPickers(boolean useJavaIO) { + boolean enabled = useJavaIO || Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; + prefPathVideo.setEnabled(enabled); + prefPathAudio.setEnabled(enabled); + } + + private boolean usingJavaIO() { + return DOWNLOAD_STORAGE_API_DEFAULT.equals( + defaultPreferences.getString(DOWNLOAD_STORAGE_API, DOWNLOAD_STORAGE_API_DEFAULT) + ); + } + + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + private void forgetSAFTree(String prefKey) { + + String oldPath = defaultPreferences.getString(prefKey, ""); + + if (oldPath != null && !oldPath.isEmpty() && oldPath.charAt(0) != File.separatorChar) { + try { + StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(ctx, Uri.parse(oldPath), null); + if (!mainStorage.isDirect()) { + mainStorage.revokePermissions(); + Log.i(TAG, "revokePermissions() [uri=" + oldPath + "] ¡success!"); + } + } catch (IOException err) { + Log.e(TAG, "Error revoking Tree uri permissions [uri=" + oldPath + "]", err); + } + } + } + + private void showMessageDialog(@StringRes int title, @StringRes int message) { + AlertDialog.Builder msg = new AlertDialog.Builder(ctx); + msg.setTitle(title); + msg.setMessage(message); + msg.setPositiveButton(android.R.string.ok, null); + msg.show(); } @Override @@ -51,17 +151,31 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { Log.d(TAG, "onPreferenceTreeClick() called with: preference = [" + preference + "]"); } - if (preference.getKey().equals(DOWNLOAD_PATH_PREFERENCE) - || preference.getKey().equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) { - Intent i = new Intent(getActivity(), FilePickerActivityHelper.class) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true) - .putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_DIR); - if (preference.getKey().equals(DOWNLOAD_PATH_PREFERENCE)) { - startActivityForResult(i, REQUEST_DOWNLOAD_PATH); - } else if (preference.getKey().equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) { - startActivityForResult(i, REQUEST_DOWNLOAD_AUDIO_PATH); + String key = preference.getKey(); + + if (key.equals(DOWNLOAD_PATH_VIDEO_PREFERENCE) || key.equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) { + boolean safPick = !usingJavaIO(); + + int request = 0; + if (key.equals(DOWNLOAD_PATH_VIDEO_PREFERENCE)) { + request = REQUEST_DOWNLOAD_VIDEO_PATH; + } else if (key.equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) { + request = REQUEST_DOWNLOAD_AUDIO_PATH; } + + Intent i; + if (safPick && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + .putExtra("android.content.extra.SHOW_ADVANCED", true) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | StoredDirectoryHelper.PERMISSION_FLAGS); + } else { + i = new Intent(getActivity(), FilePickerActivityHelper.class) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true) + .putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_DIR); + } + + startActivityForResult(i, request); } return super.onPreferenceTreeClick(preference); @@ -71,25 +185,50 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (DEBUG) { - Log.d(TAG, "onActivityResult() called with: requestCode = [" + requestCode + "], resultCode = [" + resultCode + "], data = [" + data + "]"); + Log.d(TAG, "onActivityResult() called with: requestCode = [" + requestCode + "], " + + "resultCode = [" + resultCode + "], data = [" + data + "]" + ); } - if ((requestCode == REQUEST_DOWNLOAD_PATH || requestCode == REQUEST_DOWNLOAD_AUDIO_PATH) - && resultCode == Activity.RESULT_OK && data.getData() != null) { - String key = getString(requestCode == REQUEST_DOWNLOAD_PATH ? R.string.download_path_key : R.string.download_path_audio_key); - String path = Utils.getFileForUri(data.getData()).getAbsolutePath(); + if (resultCode != Activity.RESULT_OK) return; - defaultPreferences.edit().putString(key, path).apply(); + String key; + if (requestCode == REQUEST_DOWNLOAD_VIDEO_PATH) + key = DOWNLOAD_PATH_VIDEO_PREFERENCE; + else if (requestCode == REQUEST_DOWNLOAD_AUDIO_PATH) + key = DOWNLOAD_PATH_AUDIO_PREFERENCE; + else + return; + + Uri uri = data.getData(); + if (uri == null) { + showMessageDialog(R.string.general_error, R.string.invalid_directory); + return; + } + + if (!usingJavaIO() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + // steps: + // 1. acquire permissions on the new save path + // 2. save the new path, if step(1) was successful + + try { + StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(ctx, uri, null); + mainStorage.acquirePermissions(); + Log.i(TAG, "acquirePermissions() [uri=" + uri.toString() + "] ¡success!"); + } catch (IOException err) { + Log.e(TAG, "Error acquiring permissions on " + uri.toString()); + showMessageDialog(R.string.general_error, R.string.no_available_dir); + return; + } + + defaultPreferences.edit().putString(key, uri.toString()).apply(); + } else { + defaultPreferences.edit().putString(key, uri.toString()).apply(); updatePreferencesSummary(); - File target = new File(path); - if (!target.canWrite()) { - AlertDialog.Builder msg = new AlertDialog.Builder(getContext()); - msg.setTitle(R.string.download_to_sdcard_error_title); - msg.setMessage(R.string.download_to_sdcard_error_message); - msg.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> { }); - msg.show(); - } + File target = new File(URI.create(uri.toString())); + if (!target.canWrite()) + showMessageDialog(R.string.download_to_sdcard_error_title, R.string.download_to_sdcard_error_message); } } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java index 2a0e2645b..f153cf23a 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java +++ b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java @@ -70,37 +70,23 @@ public class NewPipeSettings { getAudioDownloadFolder(context); } - public static File getVideoDownloadFolder(Context context) { - return getDir(context, R.string.download_path_key, Environment.DIRECTORY_MOVIES); + private static void getVideoDownloadFolder(Context context) { + getDir(context, R.string.download_path_video_key, Environment.DIRECTORY_MOVIES); } - public static String getVideoDownloadPath(Context context) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - final String key = context.getString(R.string.download_path_key); - return prefs.getString(key, Environment.DIRECTORY_MOVIES); + private static void getAudioDownloadFolder(Context context) { + getDir(context, R.string.download_path_audio_key, Environment.DIRECTORY_MUSIC); } - public static File getAudioDownloadFolder(Context context) { - return getDir(context, R.string.download_path_audio_key, Environment.DIRECTORY_MUSIC); - } - - public static String getAudioDownloadPath(Context context) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - final String key = context.getString(R.string.download_path_audio_key); - return prefs.getString(key, Environment.DIRECTORY_MUSIC); - } - - private static File getDir(Context context, int keyID, String defaultDirectoryName) { + private static void getDir(Context context, int keyID, String defaultDirectoryName) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); final String key = context.getString(keyID); String downloadPath = prefs.getString(key, null); - if ((downloadPath != null) && (!downloadPath.isEmpty())) return new File(downloadPath.trim()); + if ((downloadPath != null) && (!downloadPath.isEmpty())) return; - final File dir = getDir(defaultDirectoryName); SharedPreferences.Editor spEditor = prefs.edit(); - spEditor.putString(key, getNewPipeChildFolderPathForDir(dir)); + spEditor.putString(key, getNewPipeChildFolderPathForDir(getDir(defaultDirectoryName))); spEditor.apply(); - return dir; } @NonNull @@ -110,8 +96,13 @@ public class NewPipeSettings { public static void resetDownloadFolders(Context context) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + + prefs.edit() + .putString(context.getString(R.string.downloads_storage_api), context.getString(R.string.downloads_storage_api_default)) + .apply(); + resetDownloadFolder(prefs, context.getString(R.string.download_path_audio_key), Environment.DIRECTORY_MUSIC); - resetDownloadFolder(prefs, context.getString(R.string.download_path_key), Environment.DIRECTORY_MOVIES); + resetDownloadFolder(prefs, context.getString(R.string.download_path_video_key), Environment.DIRECTORY_MOVIES); } private static void resetDownloadFolder(SharedPreferences prefs, String key, String defaultDirectoryName) { diff --git a/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java b/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java index 5a4efbe32..61f793e5d 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java @@ -120,7 +120,7 @@ public class Mp4FromDashWriter { parsed = true; for (SharpStream src : sourceTracks) { - src.dispose(); + src.close(); } tracks = null; diff --git a/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java b/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java index eba2bbb87..26b9cbebf 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java @@ -107,7 +107,7 @@ public class WebMWriter { parsed = true; for (SharpStream src : sourceTracks) { - src.dispose(); + src.close(); } sourceTracks = null; diff --git a/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java b/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java index ea2f60837..5950ba3dd 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java +++ b/app/src/main/java/org/schabi/newpipe/streams/io/SharpStream.java @@ -1,11 +1,12 @@ package org.schabi.newpipe.streams.io; +import java.io.Closeable; import java.io.IOException; /** * based on c# */ -public abstract class SharpStream { +public abstract class SharpStream implements Closeable { public abstract int read() throws IOException; @@ -19,9 +20,10 @@ public abstract class SharpStream { public abstract void rewind() throws IOException; - public abstract void dispose(); + public abstract boolean isClosed(); - public abstract boolean isDisposed(); + @Override + public abstract void close(); public abstract boolean canRewind(); @@ -54,4 +56,8 @@ public abstract class SharpStream { public void seek(long offset) throws IOException { throw new IOException("Not implemented"); } + + public long length() throws IOException { + throw new UnsupportedOperationException("Unsupported operation"); + } } diff --git a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java index abc934878..1e05983d8 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java @@ -3,10 +3,10 @@ package us.shandian.giga.get; import android.support.annotation.NonNull; import android.util.Log; -import java.io.File; +import org.schabi.newpipe.streams.io.SharpStream; + import java.io.IOException; import java.io.InterruptedIOException; -import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.nio.channels.ClosedByInterruptException; @@ -111,34 +111,10 @@ public class DownloadInitializer extends Thread { if (!mMission.running || Thread.interrupted()) return; } - File file; - if (mMission.current == 0) { - file = new File(mMission.location); - if (!Utility.mkdir(file, true)) { - mMission.notifyError(DownloadMission.ERROR_PATH_CREATION, null); - return; - } - - file = new File(file, mMission.name); - - // if the name is used by another process, delete it - if (file.exists() && !file.isFile() && !file.delete()) { - mMission.notifyError(DownloadMission.ERROR_FILE_CREATION, null); - return; - } - - if (!file.exists() && !file.createNewFile()) { - mMission.notifyError(DownloadMission.ERROR_FILE_CREATION, null); - return; - } - } else { - file = new File(mMission.location, mMission.name); - } - - RandomAccessFile af = new RandomAccessFile(file, "rw"); - af.setLength(mMission.offsets[mMission.current] + mMission.length); - af.seek(mMission.offsets[mMission.current]); - af.close(); + SharpStream fs = mMission.storage.getStream(); + fs.setLength(mMission.offsets[mMission.current] + mMission.length); + fs.seek(mMission.offsets[mMission.current]); + fs.close(); if (!mMission.running || Thread.interrupted()) return; diff --git a/app/src/main/java/us/shandian/giga/get/DownloadMission.java b/app/src/main/java/us/shandian/giga/get/DownloadMission.java index b8849482a..9ec3418b0 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMission.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMission.java @@ -2,6 +2,7 @@ package us.shandian.giga.get; import android.os.Handler; import android.os.Message; +import android.support.annotation.NonNull; import android.util.Log; import java.io.File; @@ -17,6 +18,7 @@ import java.util.List; import javax.net.ssl.SSLException; +import us.shandian.giga.io.StoredFileHelper; import us.shandian.giga.postprocessing.Postprocessing; import us.shandian.giga.service.DownloadManagerService; import us.shandian.giga.util.Utility; @@ -24,7 +26,7 @@ import us.shandian.giga.util.Utility; import static org.schabi.newpipe.BuildConfig.DEBUG; public class DownloadMission extends Mission { - private static final long serialVersionUID = 3L;// last bump: 8 november 2018 + private static final long serialVersionUID = 4L;// last bump: 27 march 2019 static final int BUFFER_SIZE = 64 * 1024; final static int BLOCK_SIZE = 512 * 1024; @@ -43,6 +45,7 @@ public class DownloadMission extends Mission { public static final int ERROR_POSTPROCESSING_STOPPED = 1008; public static final int ERROR_POSTPROCESSING_HOLD = 1009; public static final int ERROR_INSUFFICIENT_STORAGE = 1010; + public static final int ERROR_PROGRESS_LOST = 1011; public static final int ERROR_HTTP_NO_CONTENT = 204; public static final int ERROR_HTTP_UNSUPPORTED_RANGE = 206; @@ -71,16 +74,6 @@ public class DownloadMission extends Mission { */ public long[] offsets; - /** - * The post-processing algorithm arguments - */ - public String[] postprocessingArgs; - - /** - * The post-processing algorithm name - */ - public String postprocessingName; - /** * Indicates if the post-processing state: * 0: ready @@ -88,12 +81,12 @@ public class DownloadMission extends Mission { * 2: completed * 3: hold */ - public volatile int postprocessingState; + public volatile int psState; /** - * Indicate if the post-processing algorithm works on the same file + * the post-processing algorithm instance */ - public boolean postprocessingThis; + public transient Postprocessing psAlgorithm; /** * The current resource to download, see {@code urls[current]} and {@code offsets[current]} @@ -138,36 +131,23 @@ public class DownloadMission extends Mission { public transient volatile Thread[] threads = new Thread[0]; private transient Thread init = null; - protected DownloadMission() { } - public DownloadMission(String url, String name, String location, char kind) { - this(new String[]{url}, name, location, kind, null, null); - } - - public DownloadMission(String[] urls, String name, String location, char kind, String postprocessingName, String[] postprocessingArgs) { - if (name == null) throw new NullPointerException("name is null"); - if (name.isEmpty()) throw new IllegalArgumentException("name is empty"); + public DownloadMission(String[] urls, StoredFileHelper storage, char kind, Postprocessing psInstance) { if (urls == null) throw new NullPointerException("urls is null"); if (urls.length < 1) throw new IllegalArgumentException("urls is empty"); - if (location == null) throw new NullPointerException("location is null"); - if (location.isEmpty()) throw new IllegalArgumentException("location is empty"); this.urls = urls; - this.name = name; - this.location = location; this.kind = kind; this.offsets = new long[urls.length]; this.enqueued = true; this.maxRetry = 3; + this.storage = storage; - if (postprocessingName != null) { - Postprocessing algorithm = Postprocessing.getAlgorithm(postprocessingName, null); - this.postprocessingThis = algorithm.worksOnSameFile; - this.offsets[0] = algorithm.recommendedReserve; - this.postprocessingName = postprocessingName; - this.postprocessingArgs = postprocessingArgs; + if (psInstance != null) { + this.psAlgorithm = psInstance; + this.offsets[0] = psInstance.recommendedReserve; } else { if (DEBUG && urls.length > 1) { Log.w(TAG, "mission created with multiple urls ¿missing post-processing algorithm?"); @@ -359,22 +339,12 @@ public class DownloadMission extends Mission { Log.e(TAG, "notifyError() code = " + code, err); if (err instanceof IOException) { - if (err.getMessage().contains("Permission denied")) { + if (storage.canWrite() || err.getMessage().contains("Permission denied")) { code = ERROR_PERMISSION_DENIED; err = null; - } else if (err.getMessage().contains("write failed: ENOSPC")) { + } else if (err.getMessage().contains("ENOSPC")) { code = ERROR_INSUFFICIENT_STORAGE; err = null; - } else { - try { - File storage = new File(location); - if (storage.canWrite() && storage.getUsableSpace() < (getLength() - done)) { - code = ERROR_INSUFFICIENT_STORAGE; - err = null; - } - } catch (SecurityException e) { - // is a permission error - } } } @@ -433,11 +403,11 @@ public class DownloadMission extends Mission { action = "Failed"; } - Log.d(TAG, action + " postprocessing on " + location + File.separator + name); + Log.d(TAG, action + " postprocessing on " + storage.getName()); synchronized (blockState) { // don't return without fully write the current state - postprocessingState = state; + psState = state; Utility.writeToFile(metadata, DownloadMission.this); } } @@ -456,7 +426,7 @@ public class DownloadMission extends Mission { running = true; errCode = ERROR_NOTHING; - if (current >= urls.length && postprocessingName != null) { + if (current >= urls.length && psAlgorithm != null) { runAsync(1, () -> { if (doPostprocessing()) { running = false; @@ -593,7 +563,7 @@ public class DownloadMission extends Mission { * @return true, otherwise, false */ public boolean isFinished() { - return current >= urls.length && (postprocessingName == null || postprocessingState == 2); + return current >= urls.length && (psAlgorithm == null || psState == 2); } /** @@ -602,7 +572,13 @@ public class DownloadMission extends Mission { * @return {@code true} if this mission is unrecoverable */ public boolean isPsFailed() { - return postprocessingName != null && errCode == DownloadMission.ERROR_POSTPROCESSING && postprocessingThis; + switch (errCode) { + case ERROR_POSTPROCESSING: + case ERROR_POSTPROCESSING_STOPPED: + return psAlgorithm.worksOnSameFile; + } + + return false; } /** @@ -611,7 +587,7 @@ public class DownloadMission extends Mission { * @return true, otherwise, false */ public boolean isPsRunning() { - return postprocessingName != null && (postprocessingState == 1 || postprocessingState == 3); + return psAlgorithm != null && (psState == 1 || psState == 3); } /** @@ -625,7 +601,7 @@ public class DownloadMission extends Mission { public long getLength() { long calculated; - if (postprocessingState == 1 || postprocessingState == 3) { + if (psState == 1 || psState == 3) { calculated = length; } else { calculated = offsets[current < offsets.length ? current : (offsets.length - 1)] + length; @@ -652,38 +628,60 @@ public class DownloadMission extends Mission { * @param recover {@code true} to retry, otherwise, {@code false} to cancel */ public void psContinue(boolean recover) { - postprocessingState = 1; + psState = 1; errCode = recover ? ERROR_NOTHING : ERROR_POSTPROCESSING; threads[0].interrupt(); } + /** + * changes the StoredFileHelper for another and saves the changes to the metadata file + * + * @param newStorage the new StoredFileHelper instance to use + */ + public void changeStorage(@NonNull StoredFileHelper newStorage) { + storage = newStorage; + // commit changes on the metadata file + runAsync(-2, this::writeThisToFile); + } + + /** + * Indicates whatever the backed storage is invalid + * + * @return {@code true}, if storage is invalid and cannot be used + */ + public boolean hasInvalidStorage() { + return errCode == ERROR_PROGRESS_LOST || storage == null || storage.isInvalid(); + } + + /** + * Indicates whatever is possible to start the mission + * + * @return {@code true} is this mission is "sane", otherwise, {@code false} + */ + public boolean canDownload() { + return !(isPsFailed() || errCode == ERROR_POSTPROCESSING_HOLD) && !isFinished() && !hasInvalidStorage(); + } + private boolean doPostprocessing() { - if (postprocessingName == null || postprocessingState == 2) return true; + if (psAlgorithm == null || psState == 2) return true; notifyPostProcessing(1); notifyProgress(0); if (DEBUG) - Thread.currentThread().setName("[" + TAG + "] post-processing = " + postprocessingName + " filename = " + name); + Thread.currentThread().setName("[" + TAG + "] ps = " + + psAlgorithm.getClass().getSimpleName() + + " filename = " + storage.getName() + ); threads = new Thread[]{Thread.currentThread()}; Exception exception = null; try { - Postprocessing - .getAlgorithm(postprocessingName, this) - .run(); + psAlgorithm.run(this); } catch (Exception err) { - StringBuilder args = new StringBuilder(" "); - if (postprocessingArgs != null) { - for (String arg : postprocessingArgs) { - args.append(", "); - args.append(arg); - } - args.delete(0, 1); - } - Log.e(TAG, String.format("Post-processing failed. algorithm = %s args = [%s]", postprocessingName, args), err); + Log.e(TAG, "Post-processing failed. " + psAlgorithm.toString(), err); if (errCode == ERROR_NOTHING) errCode = ERROR_POSTPROCESSING; @@ -733,7 +731,7 @@ public class DownloadMission extends Mission { // >=1: any download thread if (DEBUG) { - who.setName(String.format("%s[%s] %s", TAG, id, name)); + who.setName(String.format("%s[%s] %s", TAG, id, storage.getName())); } who.start(); diff --git a/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java b/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java index 244fbd47a..ced579b20 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java @@ -2,9 +2,10 @@ package us.shandian.giga.get; import android.util.Log; -import java.io.FileNotFoundException; +import org.schabi.newpipe.streams.io.SharpStream; + +import java.io.IOException; import java.io.InputStream; -import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.nio.channels.ClosedByInterruptException; @@ -40,12 +41,12 @@ public class DownloadRunnable extends Thread { Log.d(TAG, mId + ":recovered: " + mMission.recovered); } - RandomAccessFile f; + SharpStream f; InputStream is = null; try { - f = new RandomAccessFile(mMission.getDownloadedFile(), "rw"); - } catch (FileNotFoundException e) { + f = mMission.storage.getStream(); + } catch (IOException e) { mMission.notifyError(e);// this never should happen return; } diff --git a/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java b/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java index 4bcaeaf85..1a4b5d5b6 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java @@ -4,13 +4,13 @@ import android.annotation.SuppressLint; import android.support.annotation.NonNull; import android.util.Log; +import org.schabi.newpipe.streams.io.SharpStream; + import java.io.IOException; import java.io.InputStream; -import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.nio.channels.ClosedByInterruptException; - import us.shandian.giga.util.Utility; import static org.schabi.newpipe.BuildConfig.DEBUG; @@ -22,11 +22,10 @@ public class DownloadRunnableFallback extends Thread { private static final String TAG = "DownloadRunnableFallback"; private final DownloadMission mMission; - private final int mId = 1; private int mRetryCount = 0; private InputStream mIs; - private RandomAccessFile mF; + private SharpStream mF; private HttpURLConnection mConn; DownloadRunnableFallback(@NonNull DownloadMission mission) { @@ -43,11 +42,7 @@ public class DownloadRunnableFallback extends Thread { // nothing to do } - try { - if (mF != null) mF.close(); - } catch (IOException e) { - // ¿ejected media storage? ¿file deleted? ¿storage ran out of space? - } + if (mF != null) mF.close(); } @Override @@ -67,6 +62,7 @@ public class DownloadRunnableFallback extends Thread { try { long rangeStart = (mMission.unknownLength || start < 1) ? -1 : start; + int mId = 1; mConn = mMission.openConnection(mId, rangeStart, -1); mMission.establishConnection(mId, mConn); @@ -81,7 +77,7 @@ public class DownloadRunnableFallback extends Thread { if (!mMission.unknownLength) mMission.unknownLength = Utility.getContentLength(mConn) == -1; - mF = new RandomAccessFile(mMission.getDownloadedFile(), "rw"); + mF = mMission.storage.getStream(); mF.seek(mMission.offsets[mMission.current] + start); mIs = mConn.getInputStream(); diff --git a/app/src/main/java/us/shandian/giga/get/FinishedMission.java b/app/src/main/java/us/shandian/giga/get/FinishedMission.java index b7d6908a5..5540b44a1 100644 --- a/app/src/main/java/us/shandian/giga/get/FinishedMission.java +++ b/app/src/main/java/us/shandian/giga/get/FinishedMission.java @@ -1,16 +1,16 @@ package us.shandian.giga.get; +import android.support.annotation.NonNull; + public class FinishedMission extends Mission { public FinishedMission() { } - public FinishedMission(DownloadMission mission) { + public FinishedMission(@NonNull DownloadMission mission) { source = mission.source; length = mission.length;// ¿or mission.done? timestamp = mission.timestamp; - name = mission.name; - location = mission.location; kind = mission.kind; } } diff --git a/app/src/main/java/us/shandian/giga/get/Mission.java b/app/src/main/java/us/shandian/giga/get/Mission.java index 53c81b08b..ce201d960 100644 --- a/app/src/main/java/us/shandian/giga/get/Mission.java +++ b/app/src/main/java/us/shandian/giga/get/Mission.java @@ -1,12 +1,15 @@ package us.shandian.giga.get; -import java.io.File; +import android.net.Uri; +import android.support.annotation.NonNull; + import java.io.Serializable; -import java.text.SimpleDateFormat; import java.util.Calendar; +import us.shandian.giga.io.StoredFileHelper; + public abstract class Mission implements Serializable { - private static final long serialVersionUID = 0L;// last bump: 5 october 2018 + private static final long serialVersionUID = 1L;// last bump: 27 march 2019 /** * Source url of the resource @@ -23,28 +26,23 @@ public abstract class Mission implements Serializable { */ public long timestamp; - /** - * The filename - */ - public String name; - - /** - * The directory to store the download - */ - public String location; - /** * pre-defined content type */ public char kind; + /** + * The downloaded file + */ + public StoredFileHelper storage; + /** * get the target file on the storage * * @return File object */ - public File getDownloadedFile() { - return new File(location, name); + public Uri getDownloadedFileUri() { + return storage.getUri(); } /** @@ -53,8 +51,8 @@ public abstract class Mission implements Serializable { * @return {@code true] if and only if the file is successfully deleted, otherwise, {@code false} */ public boolean delete() { - deleted = true; - return getDownloadedFile().delete(); + if (storage != null) return storage.delete(); + return true; } /** @@ -62,10 +60,11 @@ public abstract class Mission implements Serializable { */ public transient boolean deleted = false; + @NonNull @Override public String toString() { Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(timestamp); - return "[" + calendar.getTime().toString() + "] " + location + File.separator + name; + return "[" + calendar.getTime().toString() + "] " + getDownloadedFileUri().getPath(); } } diff --git a/app/src/main/java/us/shandian/giga/get/sqlite/DownloadDataSource.java b/app/src/main/java/us/shandian/giga/get/sqlite/DownloadDataSource.java deleted file mode 100644 index 4b4d5d733..000000000 --- a/app/src/main/java/us/shandian/giga/get/sqlite/DownloadDataSource.java +++ /dev/null @@ -1,73 +0,0 @@ -package us.shandian.giga.get.sqlite; - -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.util.Log; - -import java.util.ArrayList; -import java.util.List; - -import us.shandian.giga.get.DownloadMission; -import us.shandian.giga.get.FinishedMission; -import us.shandian.giga.get.Mission; - -import static us.shandian.giga.get.sqlite.DownloadMissionHelper.KEY_LOCATION; -import static us.shandian.giga.get.sqlite.DownloadMissionHelper.KEY_NAME; -import static us.shandian.giga.get.sqlite.DownloadMissionHelper.MISSIONS_TABLE_NAME; - -public class DownloadDataSource { - - private static final String TAG = "DownloadDataSource"; - private final DownloadMissionHelper downloadMissionHelper; - - public DownloadDataSource(Context context) { - downloadMissionHelper = new DownloadMissionHelper(context); - } - - public ArrayList loadFinishedMissions() { - SQLiteDatabase database = downloadMissionHelper.getReadableDatabase(); - Cursor cursor = database.query(MISSIONS_TABLE_NAME, null, null, - null, null, null, DownloadMissionHelper.KEY_TIMESTAMP); - - int count = cursor.getCount(); - if (count == 0) return new ArrayList<>(1); - - ArrayList result = new ArrayList<>(count); - while (cursor.moveToNext()) { - result.add(DownloadMissionHelper.getMissionFromCursor(cursor)); - } - - return result; - } - - public void addMission(DownloadMission downloadMission) { - if (downloadMission == null) throw new NullPointerException("downloadMission is null"); - SQLiteDatabase database = downloadMissionHelper.getWritableDatabase(); - ContentValues values = DownloadMissionHelper.getValuesOfMission(downloadMission); - database.insert(MISSIONS_TABLE_NAME, null, values); - } - - public void deleteMission(Mission downloadMission) { - if (downloadMission == null) throw new NullPointerException("downloadMission is null"); - SQLiteDatabase database = downloadMissionHelper.getWritableDatabase(); - database.delete(MISSIONS_TABLE_NAME, - KEY_LOCATION + " = ? AND " + - KEY_NAME + " = ?", - new String[]{downloadMission.location, downloadMission.name}); - } - - public void updateMission(DownloadMission downloadMission) { - if (downloadMission == null) throw new NullPointerException("downloadMission is null"); - SQLiteDatabase database = downloadMissionHelper.getWritableDatabase(); - ContentValues values = DownloadMissionHelper.getValuesOfMission(downloadMission); - String whereClause = KEY_LOCATION + " = ? AND " + - KEY_NAME + " = ?"; - int rowsAffected = database.update(MISSIONS_TABLE_NAME, values, - whereClause, new String[]{downloadMission.location, downloadMission.name}); - if (rowsAffected != 1) { - Log.e(TAG, "Expected 1 row to be affected by update but got " + rowsAffected); - } - } -} diff --git a/app/src/main/java/us/shandian/giga/get/sqlite/DownloadMissionHelper.java b/app/src/main/java/us/shandian/giga/get/sqlite/DownloadMissionHelper.java deleted file mode 100644 index 6dadc98c8..000000000 --- a/app/src/main/java/us/shandian/giga/get/sqlite/DownloadMissionHelper.java +++ /dev/null @@ -1,112 +0,0 @@ -package us.shandian.giga.get.sqlite; - -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; - -import us.shandian.giga.get.DownloadMission; -import us.shandian.giga.get.FinishedMission; - -/** - * SQLiteHelper to store finished {@link us.shandian.giga.get.DownloadMission}'s - */ -public class DownloadMissionHelper extends SQLiteOpenHelper { - private final String TAG = "DownloadMissionHelper"; - - // TODO: use NewPipeSQLiteHelper ('s constants) when playlist branch is merged (?) - private static final String DATABASE_NAME = "downloads.db"; - - private static final int DATABASE_VERSION = 3; - - /** - * The table name of download missions - */ - static final String MISSIONS_TABLE_NAME = "download_missions"; - - /** - * The key to the directory location of the mission - */ - static final String KEY_LOCATION = "location"; - /** - * The key to the urls of a mission - */ - static final String KEY_SOURCE_URL = "url"; - /** - * The key to the name of a mission - */ - static final String KEY_NAME = "name"; - - /** - * The key to the done. - */ - static final String KEY_DONE = "bytes_downloaded"; - - static final String KEY_TIMESTAMP = "timestamp"; - - static final String KEY_KIND = "kind"; - - /** - * The statement to create the table - */ - private static final String MISSIONS_CREATE_TABLE = - "CREATE TABLE " + MISSIONS_TABLE_NAME + " (" + - KEY_LOCATION + " TEXT NOT NULL, " + - KEY_NAME + " TEXT NOT NULL, " + - KEY_SOURCE_URL + " TEXT NOT NULL, " + - KEY_DONE + " INTEGER NOT NULL, " + - KEY_TIMESTAMP + " INTEGER NOT NULL, " + - KEY_KIND + " TEXT NOT NULL, " + - " UNIQUE(" + KEY_LOCATION + ", " + KEY_NAME + "));"; - - public DownloadMissionHelper(Context context) { - super(context, DATABASE_NAME, null, DATABASE_VERSION); - } - - @Override - public void onCreate(SQLiteDatabase db) { - db.execSQL(MISSIONS_CREATE_TABLE); - } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - if (oldVersion == 2) { - db.execSQL("ALTER TABLE " + MISSIONS_TABLE_NAME + " ADD COLUMN " + KEY_KIND + " TEXT;"); - } - } - - /** - * Returns all values of the download mission as ContentValues. - * - * @param downloadMission the download mission - * @return the content values - */ - public static ContentValues getValuesOfMission(DownloadMission downloadMission) { - ContentValues values = new ContentValues(); - values.put(KEY_SOURCE_URL, downloadMission.source); - values.put(KEY_LOCATION, downloadMission.location); - values.put(KEY_NAME, downloadMission.name); - values.put(KEY_DONE, downloadMission.done); - values.put(KEY_TIMESTAMP, downloadMission.timestamp); - values.put(KEY_KIND, String.valueOf(downloadMission.kind)); - return values; - } - - public static FinishedMission getMissionFromCursor(Cursor cursor) { - if (cursor == null) throw new NullPointerException("cursor is null"); - - String kind = cursor.getString(cursor.getColumnIndex(KEY_KIND)); - if (kind == null || kind.isEmpty()) kind = "?"; - - FinishedMission mission = new FinishedMission(); - mission.name = cursor.getString(cursor.getColumnIndexOrThrow(KEY_NAME)); - mission.location = cursor.getString(cursor.getColumnIndexOrThrow(KEY_LOCATION)); - mission.source = cursor.getString(cursor.getColumnIndexOrThrow(KEY_SOURCE_URL));; - mission.length = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_DONE)); - mission.timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_TIMESTAMP)); - mission.kind = kind.charAt(0); - - return mission; - } -} diff --git a/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java b/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java new file mode 100644 index 000000000..6d63b9ff7 --- /dev/null +++ b/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java @@ -0,0 +1,223 @@ +package us.shandian.giga.get.sqlite; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.util.Log; + +import java.io.File; +import java.util.ArrayList; + +import us.shandian.giga.get.DownloadMission; +import us.shandian.giga.get.FinishedMission; +import us.shandian.giga.get.Mission; +import us.shandian.giga.io.StoredFileHelper; + +/** + * SQLite helper to store finished {@link us.shandian.giga.get.FinishedMission}'s + */ +public class FinishedMissionStore extends SQLiteOpenHelper { + + // TODO: use NewPipeSQLiteHelper ('s constants) when playlist branch is merged (?) + private static final String DATABASE_NAME = "downloads.db"; + + private static final int DATABASE_VERSION = 4; + + /** + * The table name of download missions (old) + */ + private static final String MISSIONS_TABLE_NAME_v2 = "download_missions"; + + /** + * The table name of download missions + */ + private static final String FINISHED_MISSIONS_TABLE_NAME = "finished_missions"; + + /** + * The key to the urls of a mission + */ + private static final String KEY_SOURCE = "url"; + + + /** + * The key to the done. + */ + private static final String KEY_DONE = "bytes_downloaded"; + + private static final String KEY_TIMESTAMP = "timestamp"; + + private static final String KEY_KIND = "kind"; + + private static final String KEY_PATH = "path"; + + /** + * The statement to create the table + */ + private static final String MISSIONS_CREATE_TABLE = + "CREATE TABLE " + FINISHED_MISSIONS_TABLE_NAME + " (" + + KEY_PATH + " TEXT NOT NULL, " + + KEY_SOURCE + " TEXT NOT NULL, " + + KEY_DONE + " INTEGER NOT NULL, " + + KEY_TIMESTAMP + " INTEGER NOT NULL, " + + KEY_KIND + " TEXT NOT NULL, " + + " UNIQUE(" + KEY_TIMESTAMP + ", " + KEY_PATH + "));"; + + + private Context context; + + public FinishedMissionStore(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + this.context = context; + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL(MISSIONS_CREATE_TABLE); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion == 2) { + db.execSQL("ALTER TABLE " + MISSIONS_TABLE_NAME_v2 + " ADD COLUMN " + KEY_KIND + " TEXT;"); + oldVersion++; + } + + if (oldVersion == 3) { + final String KEY_LOCATION = "location"; + final String KEY_NAME = "name"; + + db.execSQL(MISSIONS_CREATE_TABLE); + + Cursor cursor = db.query(MISSIONS_TABLE_NAME_v2, null, null, + null, null, null, KEY_TIMESTAMP); + + int count = cursor.getCount(); + if (count > 0) { + db.beginTransaction(); + while (cursor.moveToNext()) { + ContentValues values = new ContentValues(); + values.put(KEY_SOURCE, cursor.getString(cursor.getColumnIndex(KEY_SOURCE))); + values.put(KEY_DONE, cursor.getString(cursor.getColumnIndex(KEY_DONE))); + values.put(KEY_TIMESTAMP, cursor.getLong(cursor.getColumnIndex(KEY_TIMESTAMP))); + values.put(KEY_KIND, cursor.getString(cursor.getColumnIndex(KEY_KIND))); + values.put(KEY_PATH, Uri.fromFile( + new File( + cursor.getString(cursor.getColumnIndex(KEY_LOCATION)), + cursor.getString(cursor.getColumnIndex(KEY_NAME)) + ) + ).toString()); + + db.insert(FINISHED_MISSIONS_TABLE_NAME, null, values); + } + db.setTransactionSuccessful(); + db.endTransaction(); + } + + cursor.close(); + db.execSQL("DROP TABLE " + MISSIONS_TABLE_NAME_v2); + } + } + + /** + * Returns all values of the download mission as ContentValues. + * + * @param downloadMission the download mission + * @return the content values + */ + private ContentValues getValuesOfMission(@NonNull Mission downloadMission) { + ContentValues values = new ContentValues(); + values.put(KEY_SOURCE, downloadMission.source); + values.put(KEY_PATH, downloadMission.storage.getUri().toString()); + values.put(KEY_DONE, downloadMission.length); + values.put(KEY_TIMESTAMP, downloadMission.timestamp); + values.put(KEY_KIND, String.valueOf(downloadMission.kind)); + return values; + } + + private FinishedMission getMissionFromCursor(Cursor cursor) { + if (cursor == null) throw new NullPointerException("cursor is null"); + + String kind = cursor.getString(cursor.getColumnIndex(KEY_KIND)); + if (kind == null || kind.isEmpty()) kind = "?"; + + String path = cursor.getString(cursor.getColumnIndexOrThrow(KEY_PATH)); + + FinishedMission mission = new FinishedMission(); + + mission.source = cursor.getString(cursor.getColumnIndexOrThrow(KEY_SOURCE)); + mission.length = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_DONE)); + mission.timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_TIMESTAMP)); + mission.kind = kind.charAt(0); + + try { + mission.storage = new StoredFileHelper(context, Uri.parse(path), ""); + } catch (Exception e) { + Log.e("FinishedMissionStore", "failed to load the storage path of: " + path, e); + mission.storage = new StoredFileHelper(path, "", ""); + } + + return mission; + } + + + ////////////////////////////////// + // Data source methods + /////////////////////////////////// + + public ArrayList loadFinishedMissions() { + SQLiteDatabase database = getReadableDatabase(); + Cursor cursor = database.query(FINISHED_MISSIONS_TABLE_NAME, null, null, + null, null, null, KEY_TIMESTAMP + " DESC"); + + int count = cursor.getCount(); + if (count == 0) return new ArrayList<>(1); + + ArrayList result = new ArrayList<>(count); + while (cursor.moveToNext()) { + result.add(getMissionFromCursor(cursor)); + } + + return result; + } + + public void addFinishedMission(DownloadMission downloadMission) { + if (downloadMission == null) throw new NullPointerException("downloadMission is null"); + SQLiteDatabase database = getWritableDatabase(); + ContentValues values = getValuesOfMission(downloadMission); + database.insert(FINISHED_MISSIONS_TABLE_NAME, null, values); + } + + public void deleteMission(Mission mission) { + if (mission == null) throw new NullPointerException("mission is null"); + String path = mission.getDownloadedFileUri().toString(); + + SQLiteDatabase database = getWritableDatabase(); + + if (mission instanceof FinishedMission) + database.delete(FINISHED_MISSIONS_TABLE_NAME, KEY_TIMESTAMP + " = ?, " + KEY_PATH + " = ?", new String[]{path}); + else + throw new UnsupportedOperationException("DownloadMission"); + } + + public void updateMission(Mission mission) { + if (mission == null) throw new NullPointerException("mission is null"); + SQLiteDatabase database = getWritableDatabase(); + ContentValues values = getValuesOfMission(mission); + String path = mission.getDownloadedFileUri().toString(); + + int rowsAffected; + + if (mission instanceof FinishedMission) + rowsAffected = database.update(FINISHED_MISSIONS_TABLE_NAME, values, KEY_PATH + " = ?", new String[]{path}); + else + throw new UnsupportedOperationException("DownloadMission"); + + if (rowsAffected != 1) { + Log.e("FinishedMissionStore", "Expected 1 row to be affected by update but got " + rowsAffected); + } + } +} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/io/ChunkFileInputStream.java b/app/src/main/java/us/shandian/giga/io/ChunkFileInputStream.java similarity index 80% rename from app/src/main/java/us/shandian/giga/postprocessing/io/ChunkFileInputStream.java rename to app/src/main/java/us/shandian/giga/io/ChunkFileInputStream.java index ee2fcddd5..16a90fcee 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/io/ChunkFileInputStream.java +++ b/app/src/main/java/us/shandian/giga/io/ChunkFileInputStream.java @@ -1,150 +1,148 @@ -package us.shandian.giga.postprocessing.io; - -import org.schabi.newpipe.streams.io.SharpStream; - -import java.io.File; -import java.io.IOException; -import java.io.RandomAccessFile; - -public class ChunkFileInputStream extends SharpStream { - - private RandomAccessFile source; - private final long offset; - private final long length; - private long position; - - public ChunkFileInputStream(File file, long start, long end, String mode) throws IOException { - source = new RandomAccessFile(file, mode); - offset = start; - length = end - start; - position = 0; - - if (length < 1) { - source.close(); - throw new IOException("The chunk is empty or invalid"); - } - if (source.length() < end) { - try { - throw new IOException(String.format("invalid file length. expected = %s found = %s", end, source.length())); - } finally { - source.close(); - } - } - - source.seek(offset); - } - - /** - * Get absolute position on file - * - * @return the position - */ - public long getFilePointer() { - return offset + position; - } - - @Override - public int read() throws IOException { - if ((position + 1) > length) { - return 0; - } - - int res = source.read(); - if (res >= 0) { - position++; - } - - return res; - } - - @Override - public int read(byte b[]) throws IOException { - return read(b, 0, b.length); - } - - @Override - public int read(byte b[], int off, int len) throws IOException { - if ((position + len) > length) { - len = (int) (length - position); - } - if (len == 0) { - return 0; - } - - int res = source.read(b, off, len); - position += res; - - return res; - } - - @Override - public long skip(long pos) throws IOException { - pos = Math.min(pos + position, length); - - if (pos == 0) { - return 0; - } - - source.seek(offset + pos); - - long oldPos = position; - position = pos; - - return pos - oldPos; - } - - @Override - public long available() { - return (int) (length - position); - } - - @SuppressWarnings("EmptyCatchBlock") - @Override - public void dispose() { - try { - source.close(); - } catch (IOException err) { - } finally { - source = null; - } - } - - @Override - public boolean isDisposed() { - return source == null; - } - - @Override - public void rewind() throws IOException { - position = 0; - source.seek(offset); - } - - @Override - public boolean canRewind() { - return true; - } - - @Override - public boolean canRead() { - return true; - } - - @Override - public boolean canWrite() { - return false; - } - - @Override - public void write(byte value) { - } - - @Override - public void write(byte[] buffer) { - } - - @Override - public void write(byte[] buffer, int offset, int count) { - } - -} +package us.shandian.giga.io; + +import org.schabi.newpipe.streams.io.SharpStream; + +import java.io.IOException; + +public class ChunkFileInputStream extends SharpStream { + + private SharpStream source; + private final long offset; + private final long length; + private long position; + + public ChunkFileInputStream(SharpStream target, long start) throws IOException { + this(target, start, target.length()); + } + + public ChunkFileInputStream(SharpStream target, long start, long end) throws IOException { + source = target; + offset = start; + length = end - start; + position = 0; + + if (length < 1) { + source.close(); + throw new IOException("The chunk is empty or invalid"); + } + if (source.length() < end) { + try { + throw new IOException(String.format("invalid file length. expected = %s found = %s", end, source.length())); + } finally { + source.close(); + } + } + + source.seek(offset); + } + + /** + * Get absolute position on file + * + * @return the position + */ + public long getFilePointer() { + return offset + position; + } + + @Override + public int read() throws IOException { + if ((position + 1) > length) { + return 0; + } + + int res = source.read(); + if (res >= 0) { + position++; + } + + return res; + } + + @Override + public int read(byte b[]) throws IOException { + return read(b, 0, b.length); + } + + @Override + public int read(byte b[], int off, int len) throws IOException { + if ((position + len) > length) { + len = (int) (length - position); + } + if (len == 0) { + return 0; + } + + int res = source.read(b, off, len); + position += res; + + return res; + } + + @Override + public long skip(long pos) throws IOException { + pos = Math.min(pos + position, length); + + if (pos == 0) { + return 0; + } + + source.seek(offset + pos); + + long oldPos = position; + position = pos; + + return pos - oldPos; + } + + @Override + public long available() { + return (int) (length - position); + } + + @SuppressWarnings("EmptyCatchBlock") + @Override + public void close() { + source.close(); + source = null; + } + + @Override + public boolean isClosed() { + return source == null; + } + + @Override + public void rewind() throws IOException { + position = 0; + source.seek(offset); + } + + @Override + public boolean canRewind() { + return true; + } + + @Override + public boolean canRead() { + return true; + } + + @Override + public boolean canWrite() { + return false; + } + + @Override + public void write(byte value) { + } + + @Override + public void write(byte[] buffer) { + } + + @Override + public void write(byte[] buffer, int offset, int count) { + } + +} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFileWriter.java b/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java similarity index 89% rename from app/src/main/java/us/shandian/giga/postprocessing/io/CircularFileWriter.java rename to app/src/main/java/us/shandian/giga/io/CircularFileWriter.java index 4c4160fa3..650725a76 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/io/CircularFileWriter.java +++ b/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java @@ -1,4 +1,4 @@ -package us.shandian.giga.postprocessing.io; +package us.shandian.giga.io; import android.support.annotation.NonNull; @@ -7,7 +7,6 @@ import org.schabi.newpipe.streams.io.SharpStream; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; -import java.io.RandomAccessFile; public class CircularFileWriter extends SharpStream { @@ -26,7 +25,7 @@ public class CircularFileWriter extends SharpStream { private BufferedFile out; private BufferedFile aux; - public CircularFileWriter(File source, File temp, OffsetChecker checker) throws IOException { + public CircularFileWriter(SharpStream target, File temp, OffsetChecker checker) throws IOException { if (checker == null) { throw new NullPointerException("checker is null"); } @@ -38,7 +37,7 @@ public class CircularFileWriter extends SharpStream { } aux = new BufferedFile(temp); - out = new BufferedFile(source); + out = new BufferedFile(target); callback = checker; @@ -105,7 +104,7 @@ public class CircularFileWriter extends SharpStream { out.target.setLength(length); } - dispose(); + close(); return length; } @@ -114,13 +113,13 @@ public class CircularFileWriter extends SharpStream { * Close the file without flushing any buffer */ @Override - public void dispose() { + public void close() { if (out != null) { - out.dispose(); + out.close(); out = null; } if (aux != null) { - aux.dispose(); + aux.close(); aux = null; } } @@ -256,7 +255,7 @@ public class CircularFileWriter extends SharpStream { } @Override - public boolean isDisposed() { + public boolean isClosed() { return out == null; } @@ -339,30 +338,29 @@ public class CircularFileWriter extends SharpStream { class BufferedFile { - protected final RandomAccessFile target; + protected final SharpStream target; private long offset; protected long length; - private byte[] queue; + private byte[] queue = new byte[QUEUE_BUFFER_SIZE]; private int queueSize; BufferedFile(File file) throws FileNotFoundException { - queue = new byte[QUEUE_BUFFER_SIZE]; - target = new RandomAccessFile(file, "rw"); + this.target = new FileStream(file); + } + + BufferedFile(SharpStream target) { + this.target = target; } protected long getOffset() { return offset + queueSize;// absolute offset in the file } - protected void dispose() { - try { - queue = null; - target.close(); - } catch (IOException e) { - // nothing to do - } + protected void close() { + queue = null; + target.close(); } protected void write(byte b[], int off, int len) throws IOException { @@ -384,7 +382,7 @@ public class CircularFileWriter extends SharpStream { } } - protected void flush() throws IOException { + void flush() throws IOException { writeProof(queue, queueSize); offset += queueSize; queueSize = 0; @@ -404,7 +402,7 @@ public class CircularFileWriter extends SharpStream { return queue.length - queueSize; } - protected void reset() throws IOException { + void reset() throws IOException { offset = 0; length = 0; target.seek(0); @@ -415,7 +413,7 @@ public class CircularFileWriter extends SharpStream { target.seek(absoluteOffset); } - protected void writeProof(byte[] buffer, int length) throws IOException { + void writeProof(byte[] buffer, int length) throws IOException { if (onWriteError == null) { target.write(buffer, 0, length); return; @@ -436,14 +434,8 @@ public class CircularFileWriter extends SharpStream { @NonNull @Override public String toString() { - String absOffset; String absLength; - try { - absOffset = Long.toString(target.getFilePointer()); - } catch (IOException e) { - absOffset = "[" + e.getLocalizedMessage() + "]"; - } try { absLength = Long.toString(target.length()); } catch (IOException e) { @@ -451,8 +443,8 @@ public class CircularFileWriter extends SharpStream { } return String.format( - "offset=%s length=%s queue=%s absOffset=%s absLength=%s", - offset, length, queueSize, absOffset, absLength + "offset=%s length=%s queue=%s absLength=%s", + offset, length, queueSize, absLength ); } } diff --git a/app/src/main/java/us/shandian/giga/io/FileStream.java b/app/src/main/java/us/shandian/giga/io/FileStream.java new file mode 100644 index 000000000..5b2033324 --- /dev/null +++ b/app/src/main/java/us/shandian/giga/io/FileStream.java @@ -0,0 +1,131 @@ +package us.shandian.giga.io; + +import android.support.annotation.NonNull; + +import org.schabi.newpipe.streams.io.SharpStream; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; + +/** + * @author kapodamy + */ +public class FileStream extends SharpStream { + + public RandomAccessFile source; + + public FileStream(@NonNull File target) throws FileNotFoundException { + this.source = new RandomAccessFile(target, "rw"); + } + + public FileStream(@NonNull String path) throws FileNotFoundException { + this.source = new RandomAccessFile(path, "rw"); + } + + @Override + public int read() throws IOException { + return source.read(); + } + + @Override + public int read(byte b[]) throws IOException { + return source.read(b); + } + + @Override + public int read(byte b[], int off, int len) throws IOException { + return source.read(b, off, len); + } + + @Override + public long skip(long pos) throws IOException { + return source.skipBytes((int) pos); + } + + @Override + public long available() { + try { + return source.length() - source.getFilePointer(); + } catch (IOException e) { + return 0; + } + } + + @Override + public void close() { + if (source == null) return; + try { + source.close(); + } catch (IOException err) { + // nothing to do + } + source = null; + } + + @Override + public boolean isClosed() { + return source == null; + } + + @Override + public void rewind() throws IOException { + source.seek(0); + } + + @Override + public boolean canRewind() { + return true; + } + + @Override + public boolean canRead() { + return true; + } + + @Override + public boolean canWrite() { + return true; + } + + @Override + public boolean canSeek() { + return true; + } + + @Override + public boolean canSetLength() { + return true; + } + + @Override + public void write(byte value) throws IOException { + source.write(value); + } + + @Override + public void write(byte[] buffer) throws IOException { + source.write(buffer); + } + + @Override + public void write(byte[] buffer, int offset, int count) throws IOException { + source.write(buffer, offset, count); + } + + @Override + public void setLength(long length) throws IOException { + source.setLength(length); + } + + @Override + public void seek(long offset) throws IOException { + source.seek(offset); + } + + @Override + public long length() throws IOException { + return source.length(); + } +} diff --git a/app/src/main/java/us/shandian/giga/io/FileStreamSAF.java b/app/src/main/java/us/shandian/giga/io/FileStreamSAF.java new file mode 100644 index 000000000..cb4786280 --- /dev/null +++ b/app/src/main/java/us/shandian/giga/io/FileStreamSAF.java @@ -0,0 +1,140 @@ +package us.shandian.giga.io; + +import android.content.ContentResolver; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.support.annotation.NonNull; +import android.util.Log; + +import org.schabi.newpipe.streams.io.SharpStream; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.channels.FileChannel; + +public class FileStreamSAF extends SharpStream { + + private final FileInputStream in; + private final FileOutputStream out; + private final FileChannel channel; + private final ParcelFileDescriptor file; + + private boolean disposed; + + public FileStreamSAF(@NonNull ContentResolver contentResolver, Uri fileUri) throws IOException { + // Notes: + // the file must exists first + // ¡read-write mode must allow seek! + // It is not guaranteed to work with files in the cloud (virtual files), tested in local storage devices + + file = contentResolver.openFileDescriptor(fileUri, "rw"); + + if (file == null) { + throw new IOException("Cannot get the ParcelFileDescriptor for " + fileUri.toString()); + } + + in = new FileInputStream(file.getFileDescriptor()); + out = new FileOutputStream(file.getFileDescriptor()); + channel = out.getChannel();// or use in.getChannel() + } + + @Override + public int read() throws IOException { + return in.read(); + } + + @Override + public int read(byte[] buffer) throws IOException { + return in.read(buffer); + } + + @Override + public int read(byte[] buffer, int offset, int count) throws IOException { + return in.read(buffer, offset, count); + } + + @Override + public long skip(long amount) throws IOException { + return in.skip(amount);// ¿or use channel.position(channel.position() + amount)? + } + + @Override + public long available() { + try { + return in.available(); + } catch (IOException e) { + return 0;// ¡but not -1! + } + } + + @Override + public void rewind() throws IOException { + seek(0); + } + + @Override + public void close() { + try { + disposed = true; + + file.close(); + in.close(); + out.close(); + channel.close(); + } catch (IOException e) { + Log.e("FileStreamSAF", "close() error", e); + } + } + + @Override + public boolean isClosed() { + return disposed; + } + + @Override + public boolean canRewind() { + return true; + } + + @Override + public boolean canRead() { + return true; + } + + @Override + public boolean canWrite() { + return true; + } + + public boolean canSetLength() { + return true; + } + + public boolean canSeek() { + return true; + } + + @Override + public void write(byte value) throws IOException { + out.write(value); + } + + @Override + public void write(byte[] buffer) throws IOException { + out.write(buffer); + } + + @Override + public void write(byte[] buffer, int offset, int count) throws IOException { + out.write(buffer, offset, count); + } + + public void setLength(long length) throws IOException { + channel.truncate(length); + } + + public void seek(long offset) throws IOException { + channel.position(offset); + } +} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/io/SharpInputStream.java b/app/src/main/java/us/shandian/giga/io/SharpInputStream.java similarity index 91% rename from app/src/main/java/us/shandian/giga/postprocessing/io/SharpInputStream.java rename to app/src/main/java/us/shandian/giga/io/SharpInputStream.java index 586456d98..089101dfe 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/io/SharpInputStream.java +++ b/app/src/main/java/us/shandian/giga/io/SharpInputStream.java @@ -1,61 +1,61 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ -package us.shandian.giga.postprocessing.io; - -import android.support.annotation.NonNull; - -import org.schabi.newpipe.streams.io.SharpStream; - -import java.io.IOException; -import java.io.InputStream; - -/** - * Wrapper for the classic {@link java.io.InputStream} - * - * @author kapodamy - */ -public class SharpInputStream extends InputStream { - - private final SharpStream base; - - public SharpInputStream(SharpStream base) throws IOException { - if (!base.canRead()) { - throw new IOException("The provided stream is not readable"); - } - this.base = base; - } - - @Override - public int read() throws IOException { - return base.read(); - } - - @Override - public int read(@NonNull byte[] bytes) throws IOException { - return base.read(bytes); - } - - @Override - public int read(@NonNull byte[] bytes, int i, int i1) throws IOException { - return base.read(bytes, i, i1); - } - - @Override - public long skip(long l) throws IOException { - return base.skip(l); - } - - @Override - public int available() { - long res = base.available(); - return res > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) res; - } - - @Override - public void close() { - base.dispose(); - } -} +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package us.shandian.giga.io; + +import android.support.annotation.NonNull; + +import org.schabi.newpipe.streams.io.SharpStream; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Wrapper for the classic {@link java.io.InputStream} + * + * @author kapodamy + */ +public class SharpInputStream extends InputStream { + + private final SharpStream base; + + public SharpInputStream(SharpStream base) throws IOException { + if (!base.canRead()) { + throw new IOException("The provided stream is not readable"); + } + this.base = base; + } + + @Override + public int read() throws IOException { + return base.read(); + } + + @Override + public int read(@NonNull byte[] bytes) throws IOException { + return base.read(bytes); + } + + @Override + public int read(@NonNull byte[] bytes, int i, int i1) throws IOException { + return base.read(bytes, i, i1); + } + + @Override + public long skip(long l) throws IOException { + return base.skip(l); + } + + @Override + public int available() { + long res = base.available(); + return res > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) res; + } + + @Override + public void close() { + base.close(); + } +} diff --git a/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java b/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java new file mode 100644 index 000000000..f5c2fd3f5 --- /dev/null +++ b/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java @@ -0,0 +1,175 @@ +package us.shandian.giga.io; + +import android.annotation.TargetApi; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.RequiresApi; +import android.support.v4.provider.DocumentFile; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; + +public class StoredDirectoryHelper { + public final static int PERMISSION_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION; + + private File ioTree; + private DocumentFile docTree; + + private ContentResolver contentResolver; + + private String tag; + + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + public StoredDirectoryHelper(@NonNull Context context, @NonNull Uri path, String tag) throws IOException { + this.contentResolver = context.getContentResolver(); + this.tag = tag; + this.docTree = DocumentFile.fromTreeUri(context, path); + + if (this.docTree == null) + throw new IOException("Failed to create the tree from Uri"); + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + public StoredDirectoryHelper(@NonNull String location, String tag) { + ioTree = new File(location); + this.tag = tag; + } + + @Nullable + public StoredFileHelper createFile(String filename, String mime) { + StoredFileHelper storage; + + try { + if (docTree == null) { + storage = new StoredFileHelper(ioTree, filename, tag); + storage.sourceTree = Uri.fromFile(ioTree).toString(); + } else { + storage = new StoredFileHelper(docTree, contentResolver, filename, mime, tag); + storage.sourceTree = docTree.getUri().toString(); + } + } catch (IOException e) { + return null; + } + + storage.tag = tag; + + return storage; + } + + public StoredFileHelper createUniqueFile(String filename, String mime) { + ArrayList existingNames = new ArrayList<>(50); + + String ext; + + int dotIndex = filename.lastIndexOf('.'); + if (dotIndex < 0 || (dotIndex == filename.length() - 1)) { + ext = ""; + } else { + ext = filename.substring(dotIndex); + filename = filename.substring(0, dotIndex - 1); + } + + String name; + if (docTree == null) { + for (File file : ioTree.listFiles()) { + name = file.getName().toLowerCase(); + if (name.startsWith(filename)) existingNames.add(name); + } + } else { + for (DocumentFile file : docTree.listFiles()) { + name = file.getName(); + if (name == null) continue; + name = name.toLowerCase(); + if (name.startsWith(filename)) existingNames.add(name); + } + } + + boolean free = true; + String lwFilename = filename.toLowerCase(); + for (String testName : existingNames) { + if (testName.equals(lwFilename)) { + free = false; + break; + } + } + + if (free) return createFile(filename, mime); + + String[] sortedNames = existingNames.toArray(new String[0]); + Arrays.sort(sortedNames); + + String newName; + int downloadIndex = 0; + do { + newName = filename + " (" + downloadIndex + ")" + ext; + ++downloadIndex; + if (downloadIndex == 1000) { // Probably an error on our side + newName = System.currentTimeMillis() + ext; + break; + } + } while (Arrays.binarySearch(sortedNames, newName) >= 0); + + + return createFile(newName, mime); + } + + public boolean isDirect() { + return docTree == null; + } + + public Uri getUri() { + return docTree == null ? Uri.fromFile(ioTree) : docTree.getUri(); + } + + public boolean exists() { + return docTree == null ? ioTree.exists() : docTree.exists(); + } + + public String getTag() { + return tag; + } + + public void acquirePermissions() throws IOException { + if (docTree == null) return; + + try { + contentResolver.takePersistableUriPermission(docTree.getUri(), PERMISSION_FLAGS); + } catch (Throwable e) { + throw new IOException(e); + } + } + + public void revokePermissions() throws IOException { + if (docTree == null) return; + + try { + contentResolver.releasePersistableUriPermission(docTree.getUri(), PERMISSION_FLAGS); + } catch (Throwable e) { + throw new IOException(e); + } + } + + public Uri findFile(String filename) { + if (docTree == null) + return Uri.fromFile(new File(ioTree, filename)); + + // findFile() method is very slow + DocumentFile file = docTree.findFile(filename); + + return file == null ? null : file.getUri(); + } + + @NonNull + @Override + public String toString() { + return docTree == null ? Uri.fromFile(ioTree).toString() : docTree.getUri().toString(); + } + +} diff --git a/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java b/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java new file mode 100644 index 000000000..0db442f1c --- /dev/null +++ b/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java @@ -0,0 +1,301 @@ +package us.shandian.giga.io; + +import android.annotation.TargetApi; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.provider.DocumentsContract; +import android.support.annotation.NonNull; +import android.support.v4.app.Fragment; +import android.support.v4.provider.DocumentFile; + +import org.schabi.newpipe.streams.io.SharpStream; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.net.URI; + +public class StoredFileHelper implements Serializable { + private static final long serialVersionUID = 0L; + public static final String DEFAULT_MIME = "application/octet-stream"; + + private transient DocumentFile docFile; + private transient DocumentFile docTree; + private transient File ioFile; + private transient ContentResolver contentResolver; + + protected String source; + String sourceTree; + + protected String tag; + + private String srcName; + private String srcType; + + public StoredFileHelper(String filename, String mime, String tag) { + this.source = null;// this instance will be "invalid" see invalidate()/isInvalid() methods + + this.srcName = filename; + this.srcType = mime == null ? DEFAULT_MIME : mime; + + this.tag = tag; + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + StoredFileHelper(DocumentFile tree, ContentResolver contentResolver, String filename, String mime, String tag) throws IOException { + this.docTree = tree; + this.contentResolver = contentResolver; + + // this is very slow, because SAF does not allow overwrite + DocumentFile res = this.docTree.findFile(filename); + + if (res != null && res.exists() && res.isDirectory()) { + if (!res.delete()) + throw new IOException("Directory with the same name found but cannot delete"); + res = null; + } + + if (res == null) { + res = this.docTree.createFile(mime == null ? DEFAULT_MIME : mime, filename); + if (res == null) throw new IOException("Cannot create the file"); + } + + this.docFile = res; + this.source = res.getUri().toString(); + this.srcName = getName(); + this.srcType = getType(); + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + public StoredFileHelper(Context context, @NonNull Uri path, String tag) throws IOException { + this.source = path.toString(); + this.tag = tag; + + if (path.getScheme() == null || path.getScheme().equalsIgnoreCase("file")) { + this.ioFile = new File(URI.create(this.source)); + } else { + DocumentFile file = DocumentFile.fromSingleUri(context, path); + if (file == null) + throw new UnsupportedOperationException("Cannot get the file via SAF"); + + this.contentResolver = context.getContentResolver(); + this.docFile = file; + + try { + this.contentResolver.takePersistableUriPermission(docFile.getUri(), StoredDirectoryHelper.PERMISSION_FLAGS); + } catch (Exception e) { + throw new IOException(e); + } + } + + this.srcName = getName(); + this.srcType = getType(); + } + + public StoredFileHelper(File location, String filename, String tag) throws IOException { + this.ioFile = new File(location, filename); + this.tag = tag; + + if (this.ioFile.exists()) { + if (!this.ioFile.isFile() && !this.ioFile.delete()) + throw new IOException("The filename is already in use by non-file entity and cannot overwrite it"); + } else { + if (!this.ioFile.createNewFile()) + throw new IOException("Cannot create the file"); + } + + this.source = Uri.fromFile(this.ioFile).toString(); + this.srcName = getName(); + this.srcType = getType(); + } + + public static StoredFileHelper deserialize(@NonNull StoredFileHelper storage, Context context) throws IOException { + if (storage.isInvalid()) + return new StoredFileHelper(storage.srcName, storage.srcType, storage.tag); + + StoredFileHelper instance = new StoredFileHelper(context, Uri.parse(storage.source), storage.tag); + + if (storage.sourceTree != null) { + instance.docTree = DocumentFile.fromTreeUri(context, Uri.parse(instance.sourceTree)); + + if (instance.docTree == null) + throw new IOException("Cannot deserialize the tree, ¿revoked permissions?"); + } + + return instance; + } + + public static void requestSafWithFileCreation(@NonNull Fragment who, int requestCode, String filename, String mime) { + // SAF notes: + // ACTION_OPEN_DOCUMENT Do not let you create the file, useful for overwrite files + // ACTION_CREATE_DOCUMENT No overwrite support, useless the file provider resolve the conflict + + Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT) + .addCategory(Intent.CATEGORY_OPENABLE) + .setType(mime) + .putExtra(Intent.EXTRA_TITLE, filename) + .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | StoredDirectoryHelper.PERMISSION_FLAGS) + .putExtra("android.content.extra.SHOW_ADVANCED", true);// hack, show all storage disks + + who.startActivityForResult(intent, requestCode); + } + + public SharpStream getStream() throws IOException { + invalid(); + + if (docFile == null) + return new FileStream(ioFile); + else + return new FileStreamSAF(contentResolver, docFile.getUri()); + } + + /** + * Indicates whatever if is possible access using the {@code java.io} API + * + * @return {@code true} for Java I/O API, otherwise, {@code false} for Storage Access Framework + */ + public boolean isDirect() { + invalid(); + + return docFile == null; + } + + public boolean isInvalid() { + return source == null; + } + + public Uri getUri() { + invalid(); + + return docFile == null ? Uri.fromFile(ioFile) : docFile.getUri(); + } + + public void truncate() throws IOException { + invalid(); + + try (SharpStream fs = getStream()) { + fs.setLength(0); + } + } + + public boolean delete() { + invalid(); + + if (docFile == null) return ioFile.delete(); + + boolean res = docFile.delete(); + + try { + int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION; + contentResolver.releasePersistableUriPermission(docFile.getUri(), flags); + } catch (Exception ex) { + // ¿what happen? + } + + return res; + } + + public long length() { + invalid(); + + return docFile == null ? ioFile.length() : docFile.length(); + } + + public boolean canWrite() { + if (source == null) return false; + return docFile == null ? ioFile.canWrite() : docFile.canWrite(); + } + + public File getIOFile() { + return ioFile; + } + + public String getName() { + if (source == null) return srcName; + return docFile == null ? ioFile.getName() : docFile.getName(); + } + + public String getType() { + if (source == null) return srcType; + return docFile == null ? DEFAULT_MIME : docFile.getType();// not obligatory for Java IO + } + + public String getTag() { + return tag; + } + + public boolean existsAsFile() { + if (source == null) return false; + + boolean exists = docFile == null ? ioFile.exists() : docFile.exists(); + boolean asFile = docFile == null ? ioFile.isFile() : docFile.isFile();// ¿docFile.isVirtual() means is no-physical? + + return exists && asFile; + } + + public boolean create() { + invalid(); + + if (docFile == null) { + try { + return ioFile.createNewFile(); + } catch (IOException e) { + return false; + } + } + + if (docTree == null || docFile.getName() == null) return false; + + DocumentFile res = docTree.createFile(docFile.getName(), docFile.getType() == null ? DEFAULT_MIME : docFile.getType()); + if (res == null) return false; + + docFile = res; + return true; + } + + public void invalidate() { + if (source == null) return; + + srcName = getName(); + srcType = getType(); + + source = null; + + sourceTree = null; + docTree = null; + docFile = null; + ioFile = null; + contentResolver = null; + } + + private void invalid() { + if (source == null) + throw new IllegalStateException("In invalid state"); + } + + public boolean equals(StoredFileHelper storage) { + if (this.isInvalid() != storage.isInvalid()) return false; + if (this.isDirect() != storage.isDirect()) return false; + + if (this.isDirect()) + return this.ioFile.getPath().equalsIgnoreCase(storage.ioFile.getPath()); + + return DocumentsContract.getDocumentId( + this.docFile.getUri() + ).equalsIgnoreCase(DocumentsContract.getDocumentId( + storage.docFile.getUri() + )); + } + + @NonNull + @Override + public String toString() { + if (source == null) + return "[Invalid state] name=" + srcName + " type=" + srcType + " tag=" + tag; + else + return "sourceFile=" + source + " treeSource=" + (sourceTree == null ? "" : sourceTree) + " tag=" + tag; + } +} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/M4aNoDash.java b/app/src/main/java/us/shandian/giga/postprocessing/M4aNoDash.java index fa0c2c7ae..ee7e4cba1 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/M4aNoDash.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/M4aNoDash.java @@ -6,12 +6,10 @@ import org.schabi.newpipe.streams.io.SharpStream; import java.io.IOException; -import us.shandian.giga.get.DownloadMission; - public class M4aNoDash extends Postprocessing { - M4aNoDash(DownloadMission mission) { - super(mission, 0, true); + M4aNoDash() { + super(0, true); } @Override diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Mp4FromDashMuxer.java b/app/src/main/java/us/shandian/giga/postprocessing/Mp4FromDashMuxer.java index 09f5d9661..98ab29dbb 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/Mp4FromDashMuxer.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/Mp4FromDashMuxer.java @@ -5,15 +5,13 @@ import org.schabi.newpipe.streams.io.SharpStream; import java.io.IOException; -import us.shandian.giga.get.DownloadMission; - /** * @author kapodamy */ class Mp4FromDashMuxer extends Postprocessing { - Mp4FromDashMuxer(DownloadMission mission) { - super(mission, 2 * 1024 * 1024/* 2 MiB */, true); + Mp4FromDashMuxer() { + super(2 * 1024 * 1024/* 2 MiB */, true); } @Override diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java index df8549010..7bc32ea05 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java @@ -1,6 +1,7 @@ package us.shandian.giga.postprocessing; import android.os.Message; +import android.support.annotation.NonNull; import android.util.Log; import org.schabi.newpipe.streams.io.SharpStream; @@ -9,9 +10,9 @@ import java.io.File; import java.io.IOException; import us.shandian.giga.get.DownloadMission; -import us.shandian.giga.postprocessing.io.ChunkFileInputStream; -import us.shandian.giga.postprocessing.io.CircularFileWriter; -import us.shandian.giga.postprocessing.io.CircularFileWriter.OffsetChecker; +import us.shandian.giga.io.ChunkFileInputStream; +import us.shandian.giga.io.CircularFileWriter; +import us.shandian.giga.io.CircularFileWriter.OffsetChecker; import us.shandian.giga.service.DownloadManagerService; import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING; @@ -20,30 +21,41 @@ import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION; public abstract class Postprocessing { - static final byte OK_RESULT = ERROR_NOTHING; + static transient final byte OK_RESULT = ERROR_NOTHING; - public static final String ALGORITHM_TTML_CONVERTER = "ttml"; - public static final String ALGORITHM_WEBM_MUXER = "webm"; - public static final String ALGORITHM_MP4_FROM_DASH_MUXER = "mp4D-mp4"; - public static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a"; + public transient static final String ALGORITHM_TTML_CONVERTER = "ttml"; + public transient static final String ALGORITHM_WEBM_MUXER = "webm"; + public transient static final String ALGORITHM_MP4_FROM_DASH_MUXER = "mp4D-mp4"; + public transient static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a"; + + public static Postprocessing getAlgorithm(String algorithmName, String[] args) { + Postprocessing instance; - public static Postprocessing getAlgorithm(String algorithmName, DownloadMission mission) { if (null == algorithmName) { throw new NullPointerException("algorithmName"); } else switch (algorithmName) { case ALGORITHM_TTML_CONVERTER: - return new TtmlConverter(mission); + instance = new TtmlConverter(); + break; case ALGORITHM_WEBM_MUXER: - return new WebMMuxer(mission); + instance = new WebMMuxer(); + break; case ALGORITHM_MP4_FROM_DASH_MUXER: - return new Mp4FromDashMuxer(mission); + instance = new Mp4FromDashMuxer(); + break; case ALGORITHM_M4A_NO_DASH: - return new M4aNoDash(mission); + instance = new M4aNoDash(); + break; /*case "example-algorithm": - return new ExampleAlgorithm(mission);*/ + instance = new ExampleAlgorithm(mission);*/ default: throw new RuntimeException("Unimplemented post-processing algorithm: " + algorithmName); } + + instance.args = args; + instance.name = algorithmName; + + return instance; } /** @@ -61,32 +73,38 @@ public abstract class Postprocessing { /** * the download to post-process */ - protected DownloadMission mission; + protected transient DownloadMission mission; - Postprocessing(DownloadMission mission, int recommendedReserve, boolean worksOnSameFile) { - this.mission = mission; + public transient File cacheDir; + + private String[] args; + + private String name; + + Postprocessing(int recommendedReserve, boolean worksOnSameFile) { this.recommendedReserve = recommendedReserve; this.worksOnSameFile = worksOnSameFile; } - public void run() throws IOException { - File file = mission.getDownloadedFile(); + public void run(DownloadMission target) throws IOException { + this.mission = target; + File temp = null; CircularFileWriter out = null; int result; long finalLength = -1; mission.done = 0; - mission.length = file.length(); + mission.length = mission.storage.length(); if (worksOnSameFile) { ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length]; try { int i = 0; for (; i < sources.length - 1; i++) { - sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.offsets[i + 1], "rw"); + sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i], mission.offsets[i + 1]); } - sources[i] = new ChunkFileInputStream(file, mission.offsets[i], mission.getDownloadedFile().length(), "rw"); + sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i]); if (test(sources)) { for (SharpStream source : sources) source.rewind(); @@ -97,7 +115,7 @@ public abstract class Postprocessing { * WARNING: never use rewind() in any chunk after any writing (especially on first chunks) * or the CircularFileWriter can lead to unexpected results */ - if (source.isDisposed() || source.available() < 1) { + if (source.isClosed() || source.available() < 1) { continue;// the selected source is not used anymore } @@ -107,18 +125,19 @@ public abstract class Postprocessing { return -1; }; - temp = new File(mission.location, mission.name + ".tmp"); + // TODO: use Context.getCache() for this operation + temp = new File(cacheDir, mission.storage.getName() + ".tmp"); - out = new CircularFileWriter(file, temp, checker); + out = new CircularFileWriter(mission.storage.getStream(), temp, checker); out.onProgress = this::progressReport; out.onWriteError = (err) -> { - mission.postprocessingState = 3; + mission.psState = 3; mission.notifyError(ERROR_POSTPROCESSING_HOLD, err); try { synchronized (this) { - while (mission.postprocessingState == 3) + while (mission.psState == 3) wait(); } } catch (InterruptedException e) { @@ -138,12 +157,12 @@ public abstract class Postprocessing { } } finally { for (SharpStream source : sources) { - if (source != null && !source.isDisposed()) { - source.dispose(); + if (source != null && !source.isClosed()) { + source.close(); } } if (out != null) { - out.dispose(); + out.close(); } if (temp != null) { //noinspection ResultOfMethodCallIgnored @@ -164,10 +183,9 @@ public abstract class Postprocessing { mission.errObject = new RuntimeException("post-processing algorithm returned " + result); } - if (result != OK_RESULT && worksOnSameFile) { - //noinspection ResultOfMethodCallIgnored - file.delete(); - } + if (result != OK_RESULT && worksOnSameFile) mission.storage.delete(); + + this.mission = null; } /** @@ -192,11 +210,11 @@ public abstract class Postprocessing { abstract int process(SharpStream out, SharpStream... sources) throws IOException; String getArgumentAt(int index, String defaultValue) { - if (mission.postprocessingArgs == null || index >= mission.postprocessingArgs.length) { + if (args == null || index >= args.length) { return defaultValue; } - return mission.postprocessingArgs[index]; + return args[index]; } private void progressReport(long done) { @@ -209,4 +227,22 @@ public abstract class Postprocessing { mission.mHandler.sendMessage(m); } + + @NonNull + @Override + public String toString() { + StringBuilder str = new StringBuilder(); + + str.append("name=").append(name).append('['); + + if (args != null) { + for (String arg : args) { + str.append(", "); + str.append(arg); + } + str.delete(0, 1); + } + + return str.append(']').toString(); + } } diff --git a/app/src/main/java/us/shandian/giga/postprocessing/TtmlConverter.java b/app/src/main/java/us/shandian/giga/postprocessing/TtmlConverter.java index 390061840..bba0b299a 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/TtmlConverter.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/TtmlConverter.java @@ -2,8 +2,8 @@ package us.shandian.giga.postprocessing; import android.util.Log; -import org.schabi.newpipe.streams.io.SharpStream; import org.schabi.newpipe.streams.SubtitleConverter; +import org.schabi.newpipe.streams.io.SharpStream; import org.xml.sax.SAXException; import java.io.IOException; @@ -12,18 +12,15 @@ import java.text.ParseException; import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPathExpressionException; -import us.shandian.giga.get.DownloadMission; -import us.shandian.giga.postprocessing.io.SharpInputStream; - /** * @author kapodamy */ class TtmlConverter extends Postprocessing { private static final String TAG = "TtmlConverter"; - TtmlConverter(DownloadMission mission) { + TtmlConverter() { // due how XmlPullParser works, the xml is fully loaded on the ram - super(mission, 0, true); + super(0, true); } @Override diff --git a/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java b/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java index 2ffb0f08d..37295f2e3 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java @@ -7,15 +7,13 @@ import org.schabi.newpipe.streams.io.SharpStream; import java.io.IOException; -import us.shandian.giga.get.DownloadMission; - /** * @author kapodamy */ class WebMMuxer extends Postprocessing { - WebMMuxer(DownloadMission mission) { - super(mission, 2048 * 1024/* 2 MiB */, true); + WebMMuxer() { + super(2048 * 1024/* 2 MiB */, true); } @Override diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManager.java b/app/src/main/java/us/shandian/giga/service/DownloadManager.java index 58246beb1..3624fb6c2 100644 --- a/app/src/main/java/us/shandian/giga/service/DownloadManager.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManager.java @@ -13,16 +13,15 @@ import org.schabi.newpipe.R; import java.io.File; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import us.shandian.giga.get.DownloadMission; import us.shandian.giga.get.FinishedMission; import us.shandian.giga.get.Mission; -import us.shandian.giga.get.sqlite.DownloadDataSource; -import us.shandian.giga.service.DownloadManagerService.DMChecker; -import us.shandian.giga.service.DownloadManagerService.MissionCheck; +import us.shandian.giga.get.sqlite.FinishedMissionStore; +import us.shandian.giga.io.StoredDirectoryHelper; +import us.shandian.giga.io.StoredFileHelper; import us.shandian.giga.util.Utility; import static org.schabi.newpipe.BuildConfig.DEBUG; @@ -36,7 +35,10 @@ public class DownloadManager { public final static int SPECIAL_PENDING = 1; public final static int SPECIAL_FINISHED = 2; - private final DownloadDataSource mDownloadDataSource; + static final String TAG_AUDIO = "audio"; + static final String TAG_VIDEO = "video"; + + private final FinishedMissionStore mFinishedMissionStore; private final ArrayList mMissionsPending = new ArrayList<>(); private final ArrayList mMissionsFinished; @@ -51,6 +53,9 @@ public class DownloadManager { boolean mPrefQueueLimit; private boolean mSelfMissionsControl; + StoredDirectoryHelper mMainStorageAudio; + StoredDirectoryHelper mMainStorageVideo; + /** * Create a new instance * @@ -62,7 +67,7 @@ public class DownloadManager { Log.d(TAG, "new DownloadManager instance. 0x" + Integer.toHexString(this.hashCode())); } - mDownloadDataSource = new DownloadDataSource(context); + mFinishedMissionStore = new FinishedMissionStore(context); mHandler = handler; mMissionsFinished = loadFinishedMissions(); mPendingMissionsDir = getPendingDir(context); @@ -71,7 +76,7 @@ public class DownloadManager { throw new RuntimeException("failed to create pending_downloads in data directory"); } - loadPendingMissions(); + loadPendingMissions(context); } private static File getPendingDir(@NonNull Context context) { @@ -92,29 +97,24 @@ public class DownloadManager { * Loads finished missions from the data source */ private ArrayList loadFinishedMissions() { - ArrayList finishedMissions = mDownloadDataSource.loadFinishedMissions(); + ArrayList finishedMissions = mFinishedMissionStore.loadFinishedMissions(); - // missions always is stored by creation order, simply reverse the list - ArrayList result = new ArrayList<>(finishedMissions.size()); + // check if the files exists, otherwise, forget the download for (int i = finishedMissions.size() - 1; i >= 0; i--) { FinishedMission mission = finishedMissions.get(i); - File file = mission.getDownloadedFile(); - if (!file.isFile()) { - if (DEBUG) { - Log.d(TAG, "downloaded file removed: " + file.getAbsolutePath()); - } - mDownloadDataSource.deleteMission(mission); - continue; + if (!mission.storage.existsAsFile()) { + if (DEBUG) Log.d(TAG, "downloaded file removed: " + mission.storage.getName()); + + mFinishedMissionStore.deleteMission(mission); + finishedMissions.remove(i); } - - result.add(mission); } - return result; + return finishedMissions; } - private void loadPendingMissions() { + private void loadPendingMissions(Context ctx) { File[] subs = mPendingMissionsDir.listFiles(); if (subs == null) { @@ -142,40 +142,63 @@ public class DownloadManager { continue; } - File dl = mis.getDownloadedFile(); - boolean exists = dl.exists(); + boolean exists; + try { + mis.storage = StoredFileHelper.deserialize(mis.storage, ctx); + exists = !mis.storage.isInvalid() && mis.storage.existsAsFile(); + + } catch (Exception ex) { + Log.e(TAG, "Failed to load the file source of " + mis.storage.toString()); + mis.storage.invalidate(); + exists = false; + } if (mis.isPsRunning()) { - if (mis.postprocessingThis) { + if (mis.psAlgorithm.worksOnSameFile) { // Incomplete post-processing results in a corrupted download file // because the selected algorithm works on the same file to save space. - if (exists && dl.isFile() && !dl.delete()) + if (exists && !mis.storage.delete()) Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath()); exists = true; } - mis.postprocessingState = 0; + mis.psState = 0; mis.errCode = DownloadMission.ERROR_POSTPROCESSING_STOPPED; mis.errObject = null; - } else if (exists && !dl.isFile()) { - // probably a folder, this should never happens - if (!sub.delete()) { - Log.w(TAG, "Unable to delete serialized file: " + sub.getPath()); + } else if (!exists) { + + StoredDirectoryHelper mainStorage = getMainStorage(mis.storage.getTag()); + + if (!mis.storage.isInvalid() && !mis.storage.create()) { + // using javaIO cannot recreate the file + // using SAF in older devices (no tree available) + // + // force the user to pick again the save path + mis.storage.invalidate(); + } else if (mainStorage != null) { + // if the user has changed the save path before this download, the original save path will be lost + StoredFileHelper newStorage = mainStorage.createFile(mis.storage.getName(), mis.storage.getType()); + if (newStorage == null) + mis.storage.invalidate(); + else + mis.storage = newStorage; + } + + if (mis.isInitialized()) { + // the progress is lost, reset mission state + DownloadMission m = new DownloadMission(mis.urls, mis.storage, mis.kind, mis.psAlgorithm); + m.timestamp = mis.timestamp; + m.threadCount = mis.threadCount; + m.source = mis.source; + m.nearLength = mis.nearLength; + m.enqueued = mis.enqueued; + m.errCode = DownloadMission.ERROR_PROGRESS_LOST; + mis = m; } - continue; } - if (!exists && mis.isInitialized()) { - // downloaded file deleted, reset mission state - DownloadMission m = new DownloadMission(mis.urls, mis.name, mis.location, mis.kind, mis.postprocessingName, mis.postprocessingArgs); - m.timestamp = mis.timestamp; - m.threadCount = mis.threadCount; - m.source = mis.source; - m.nearLength = mis.nearLength; - m.setEnqueued(mis.enqueued); - mis = m; - } + if (mis.psAlgorithm != null) mis.psAlgorithm.cacheDir = ctx.getCacheDir(); mis.running = false; mis.recovered = exists; @@ -196,51 +219,15 @@ public class DownloadManager { /** * Start a new download mission * - * @param urls the list of urls to download - * @param location the location - * @param name the name of the file to create - * @param kind type of file (a: audio v: video s: subtitle ?: file-extension defined) - * @param threads the number of threads maximal used to download chunks of the file. - * @param psName the name of the required post-processing algorithm, or {@code null} to ignore. - * @param source source url of the resource - * @param psArgs the arguments for the post-processing algorithm. + * @param mission the new download mission to add and run (if possible) */ - void startMission(String[] urls, String location, String name, char kind, int threads, - String source, String psName, String[] psArgs, long nearLength) { + void startMission(DownloadMission mission) { synchronized (this) { - // check for existing pending download - DownloadMission pendingMission = getPendingMission(location, name); - - if (pendingMission != null) { - if (pendingMission.running) { - // generate unique filename (?) - try { - name = generateUniqueName(location, name); - } catch (Exception e) { - Log.e(TAG, "Unable to generate unique name", e); - name = System.currentTimeMillis() + name; - Log.i(TAG, "Using " + name); - } - } else { - // dispose the mission - mMissionsPending.remove(pendingMission); - mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED); - pendingMission.delete(); - } - } else { - // check for existing finished download and dispose (if exists) - int index = getFinishedMissionIndex(location, name); - if (index >= 0) mDownloadDataSource.deleteMission(mMissionsFinished.remove(index)); - } - - DownloadMission mission = new DownloadMission(urls, name, location, kind, psName, psArgs); mission.timestamp = System.currentTimeMillis(); - mission.threadCount = threads; - mission.source = source; mission.mHandler = mHandler; mission.maxRetry = mPrefMaxRetry; - mission.nearLength = nearLength; + // create metadata file while (true) { mission.metadata = new File(mPendingMissionsDir, String.valueOf(mission.timestamp)); if (!mission.metadata.isFile() && !mission.metadata.exists()) { @@ -261,6 +248,14 @@ public class DownloadManager { // Before continue, save the metadata in case the internet connection is not available Utility.writeToFile(mission.metadata, mission); + if (mission.storage == null) { + // noting to do here + mission.errCode = DownloadMission.ERROR_FILE_CREATION; + if (mission.errObject != null) + mission.errObject = new IOException("DownloadMission.storage == NULL"); + return; + } + boolean start = !mPrefQueueLimit || getRunningMissionsCount() < 1; if (canDownloadInCurrentNetwork() && start) { @@ -292,7 +287,7 @@ public class DownloadManager { mMissionsPending.remove(mission); } else if (mission instanceof FinishedMission) { mMissionsFinished.remove(mission); - mDownloadDataSource.deleteMission(mission); + mFinishedMissionStore.deleteMission(mission); } mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED); @@ -300,18 +295,35 @@ public class DownloadManager { } } + public void forgetMission(StoredFileHelper storage) { + synchronized (this) { + Mission mission = getAnyMission(storage); + if (mission == null) return; + + if (mission instanceof DownloadMission) { + mMissionsPending.remove(mission); + } else if (mission instanceof FinishedMission) { + mMissionsFinished.remove(mission); + mFinishedMissionStore.deleteMission(mission); + } + + mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_DELETED); + mission.storage = null; + mission.delete(); + } + } + /** - * Get a pending mission by its location and name + * Get a pending mission by its path * - * @param location the location - * @param name the name + * @param storage where the file possible is stored * @return the mission or null if no such mission exists */ @Nullable - private DownloadMission getPendingMission(String location, String name) { + private DownloadMission getPendingMission(StoredFileHelper storage) { for (DownloadMission mission : mMissionsPending) { - if (location.equalsIgnoreCase(mission.location) && name.equalsIgnoreCase(mission.name)) { + if (mission.storage.equals(storage)) { return mission; } } @@ -319,16 +331,14 @@ public class DownloadManager { } /** - * Get a finished mission by its location and name + * Get a finished mission by its path * - * @param location the location - * @param name the name + * @param storage where the file possible is stored * @return the mission index or -1 if no such mission exists */ - private int getFinishedMissionIndex(String location, String name) { + private int getFinishedMissionIndex(StoredFileHelper storage) { for (int i = 0; i < mMissionsFinished.size(); i++) { - FinishedMission mission = mMissionsFinished.get(i); - if (location.equalsIgnoreCase(mission.location) && name.equalsIgnoreCase(mission.name)) { + if (mMissionsFinished.get(i).storage.equals(storage)) { return i; } } @@ -336,12 +346,12 @@ public class DownloadManager { return -1; } - public Mission getAnyMission(String location, String name) { + private Mission getAnyMission(StoredFileHelper storage) { synchronized (this) { - Mission mission = getPendingMission(location, name); + Mission mission = getPendingMission(storage); if (mission != null) return mission; - int idx = getFinishedMissionIndex(location, name); + int idx = getFinishedMissionIndex(storage); if (idx >= 0) return mMissionsFinished.get(idx); } @@ -382,7 +392,7 @@ public class DownloadManager { synchronized (this) { for (DownloadMission mission : mMissionsPending) { - if (mission.running || mission.isPsFailed() || mission.isFinished()) continue; + if (mission.running || !mission.canDownload()) continue; flag = true; mission.start(); @@ -392,58 +402,6 @@ public class DownloadManager { if (flag) mHandler.sendEmptyMessage(DownloadManagerService.MESSAGE_PROGRESS); } - - /** - * Splits the filename into name and extension - *

- * Dots are ignored if they appear: not at all, at the beginning of the file, - * at the end of the file - * - * @param name the name to split - * @return a string array with a length of 2 containing the name and the extension - */ - private static String[] splitName(String name) { - int dotIndex = name.lastIndexOf('.'); - if (dotIndex <= 0 || (dotIndex == name.length() - 1)) { - return new String[]{name, ""}; - } else { - return new String[]{name.substring(0, dotIndex), name.substring(dotIndex + 1)}; - } - } - - /** - * Generates a unique file name. - *

- * e.g. "myName (1).txt" if the name "myName.txt" exists. - * - * @param location the location (to check for existing files) - * @param name the name of the file - * @return the unique file name - * @throws IllegalArgumentException if the location is not a directory - * @throws SecurityException if the location is not readable - */ - private static String generateUniqueName(String location, String name) { - if (location == null) throw new NullPointerException("location is null"); - if (name == null) throw new NullPointerException("name is null"); - File destination = new File(location); - if (!destination.isDirectory()) { - throw new IllegalArgumentException("location is not a directory: " + location); - } - final String[] nameParts = splitName(name); - String[] existingName = destination.list((dir, name1) -> name1.startsWith(nameParts[0])); - Arrays.sort(existingName); - String newName; - int downloadIndex = 0; - do { - newName = nameParts[0] + " (" + downloadIndex + ")." + nameParts[1]; - ++downloadIndex; - if (downloadIndex == 1000) { // Probably an error on our side - throw new RuntimeException("Too many existing files"); - } - } while (Arrays.binarySearch(existingName, newName) >= 0); - return newName; - } - /** * Set a pending download as finished * @@ -453,7 +411,7 @@ public class DownloadManager { synchronized (this) { mMissionsPending.remove(mission); mMissionsFinished.add(0, new FinishedMission(mission)); - mDownloadDataSource.addMission(mission); + mFinishedMissionStore.addFinishedMission(mission); } } @@ -474,7 +432,8 @@ public class DownloadManager { boolean flag = false; for (DownloadMission mission : mMissionsPending) { - if (mission.running || !mission.enqueued || mission.isFinished()) continue; + if (mission.running || !mission.enqueued || mission.isFinished() || mission.hasInvalidStorage()) + continue; resumeMission(mission); if (mPrefQueueLimit) return true; @@ -496,7 +455,7 @@ public class DownloadManager { public void forgetFinishedDownloads() { synchronized (this) { for (FinishedMission mission : mMissionsFinished) { - mDownloadDataSource.deleteMission(mission); + mFinishedMissionStore.deleteMission(mission); } mMissionsFinished.clear(); } @@ -523,7 +482,7 @@ public class DownloadManager { int paused = 0; synchronized (this) { for (DownloadMission mission : mMissionsPending) { - if (mission.isFinished() || mission.isPsRunning()) continue; + if (!mission.canDownload() || mission.isPsRunning()) continue; if (mission.running && isMetered) { paused++; @@ -565,24 +524,32 @@ public class DownloadManager { ), Toast.LENGTH_LONG).show(); } - void checkForRunningMission(String location, String name, DMChecker check) { - MissionCheck result = MissionCheck.None; - + public MissionState checkForExistingMission(StoredFileHelper storage) { synchronized (this) { - DownloadMission pending = getPendingMission(location, name); + DownloadMission pending = getPendingMission(storage); if (pending == null) { - if (getFinishedMissionIndex(location, name) >= 0) result = MissionCheck.Finished; + if (getFinishedMissionIndex(storage) >= 0) return MissionState.Finished; } else { if (pending.isFinished()) { - result = MissionCheck.Finished;// this never should happen (race-condition) + return MissionState.Finished;// this never should happen (race-condition) } else { - result = pending.running ? MissionCheck.PendingRunning : MissionCheck.Pending; + return pending.running ? MissionState.PendingRunning : MissionState.Pending; } } } - check.callback(result); + return MissionState.None; + } + + @Nullable + private StoredDirectoryHelper getMainStorage(@NonNull String tag) { + if (tag.equals(TAG_AUDIO)) return mMainStorageAudio; + if (tag.equals(TAG_VIDEO)) return mMainStorageVideo; + + Log.w(TAG, "Unknown download category, not [audio video]: " + String.valueOf(tag)); + + return null;// this never should happen } public class MissionIterator extends DiffUtil.Callback { @@ -689,7 +656,7 @@ public class DownloadManager { synchronized (DownloadManager.this) { for (DownloadMission mission : mMissionsPending) { - if (hidden.contains(mission) || mission.isPsFailed() || mission.isFinished()) + if (hidden.contains(mission) || mission.canDownload()) continue; if (mission.running) @@ -720,7 +687,14 @@ public class DownloadManager { @Override public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { - return areItemsTheSame(oldItemPosition, newItemPosition); + Object x = snapshot.get(oldItemPosition); + Object y = current.get(newItemPosition); + + if (x instanceof Mission && y instanceof Mission) { + return ((Mission) x).storage.equals(((Mission) y).storage); + } + + return false; } } diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java index be1e20dd6..deed9e8e3 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -6,11 +6,9 @@ import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.BroadcastReceiver; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.ServiceConnection; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -21,12 +19,14 @@ import android.net.NetworkRequest; import android.net.Uri; import android.os.Binder; import android.os.Build; +import android.os.Environment; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.preference.PreferenceManager; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat.Builder; import android.support.v4.content.PermissionChecker; @@ -39,9 +39,13 @@ import org.schabi.newpipe.download.DownloadActivity; import org.schabi.newpipe.player.helper.LockManager; import java.io.File; +import java.io.IOException; import java.util.ArrayList; import us.shandian.giga.get.DownloadMission; +import us.shandian.giga.io.StoredDirectoryHelper; +import us.shandian.giga.io.StoredFileHelper; +import us.shandian.giga.postprocessing.Postprocessing; import us.shandian.giga.service.DownloadManager.NetworkState; import static org.schabi.newpipe.BuildConfig.APPLICATION_ID; @@ -61,19 +65,19 @@ public class DownloadManagerService extends Service { private static final int DOWNLOADS_NOTIFICATION_ID = 1001; private static final String EXTRA_URLS = "DownloadManagerService.extra.urls"; - private static final String EXTRA_NAME = "DownloadManagerService.extra.name"; - private static final String EXTRA_LOCATION = "DownloadManagerService.extra.location"; + private static final String EXTRA_PATH = "DownloadManagerService.extra.path"; private static final String EXTRA_KIND = "DownloadManagerService.extra.kind"; private static final String EXTRA_THREADS = "DownloadManagerService.extra.threads"; private static final String EXTRA_POSTPROCESSING_NAME = "DownloadManagerService.extra.postprocessingName"; private static final String EXTRA_POSTPROCESSING_ARGS = "DownloadManagerService.extra.postprocessingArgs"; private static final String EXTRA_SOURCE = "DownloadManagerService.extra.source"; private static final String EXTRA_NEAR_LENGTH = "DownloadManagerService.extra.nearLength"; + private static final String EXTRA_MAIN_STORAGE_TAG = "DownloadManagerService.extra.tag"; private static final String ACTION_RESET_DOWNLOAD_FINISHED = APPLICATION_ID + ".reset_download_finished"; private static final String ACTION_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_finished"; - private DMBinder mBinder; + private DownloadManagerBinder mBinder; private DownloadManager mManager; private Notification mNotification; private Handler mHandler; @@ -110,10 +114,10 @@ public class DownloadManagerService extends Service { /** * notify media scanner on downloaded media file ... * - * @param file the downloaded file + * @param file the downloaded file uri */ - private void notifyMediaScanner(File file) { - sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file))); + private void notifyMediaScanner(Uri file) { + sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, file)); } @Override @@ -124,7 +128,7 @@ public class DownloadManagerService extends Service { Log.d(TAG, "onCreate"); } - mBinder = new DMBinder(); + mBinder = new DownloadManagerBinder(); mHandler = new Handler(Looper.myLooper()) { @Override public void handleMessage(Message msg) { @@ -186,10 +190,12 @@ public class DownloadManagerService extends Service { handlePreferenceChange(mPrefs, getString(R.string.downloads_queue_limit)); mLock = new LockManager(this); + + setupStorageAPI(true); } @Override - public int onStartCommand(Intent intent, int flags, int startId) { + public int onStartCommand(final Intent intent, int flags, int startId) { if (DEBUG) { Log.d(TAG, intent == null ? "Restarting" : "Starting"); } @@ -200,20 +206,7 @@ public class DownloadManagerService extends Service { String action = intent.getAction(); if (action != null) { if (action.equals(Intent.ACTION_RUN)) { - String[] urls = intent.getStringArrayExtra(EXTRA_URLS); - String name = intent.getStringExtra(EXTRA_NAME); - String location = intent.getStringExtra(EXTRA_LOCATION); - int threads = intent.getIntExtra(EXTRA_THREADS, 1); - char kind = intent.getCharExtra(EXTRA_KIND, '?'); - String psName = intent.getStringExtra(EXTRA_POSTPROCESSING_NAME); - String[] psArgs = intent.getStringArrayExtra(EXTRA_POSTPROCESSING_ARGS); - String source = intent.getStringExtra(EXTRA_SOURCE); - long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0); - - handleConnectivityState(true);// first check the actual network status - - mHandler.post(() -> mManager.startMission(urls, location, name, kind, threads, source, psName, psArgs, nearLength)); - + mHandler.post(() -> startMission(intent)); } else if (downloadDoneNotification != null) { if (action.equals(ACTION_RESET_DOWNLOAD_FINISHED) || action.equals(ACTION_OPEN_DOWNLOADS_FINISHED)) { downloadDoneCount = 0; @@ -264,12 +257,12 @@ public class DownloadManagerService extends Service { @Override public IBinder onBind(Intent intent) { int permissionCheck; - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { - permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE); - if (permissionCheck == PermissionChecker.PERMISSION_DENIED) { - Toast.makeText(this, "Permission denied (read)", Toast.LENGTH_SHORT).show(); - } - } +// if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { +// permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE); +// if (permissionCheck == PermissionChecker.PERMISSION_DENIED) { +// Toast.makeText(this, "Permission denied (read)", Toast.LENGTH_SHORT).show(); +// } +// } permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE); if (permissionCheck == PermissionChecker.PERMISSION_DENIED) { @@ -284,8 +277,8 @@ public class DownloadManagerService extends Service { switch (msg.what) { case MESSAGE_FINISHED: - notifyMediaScanner(mission.getDownloadedFile()); - notifyFinishedDownload(mission.name); + notifyMediaScanner(mission.storage.getUri()); + notifyFinishedDownload(mission.storage.getName()); mManager.setFinished(mission); handleConnectivityState(false); updateForegroundState(mManager.runMissions()); @@ -344,7 +337,7 @@ public class DownloadManagerService extends Service { if (key.equals(getString(R.string.downloads_maximum_retry))) { try { String value = prefs.getString(key, getString(R.string.downloads_maximum_retry_default)); - mManager.mPrefMaxRetry = Integer.parseInt(value); + mManager.mPrefMaxRetry = value == null ? 0 : Integer.parseInt(value); } catch (Exception e) { mManager.mPrefMaxRetry = 0; } @@ -353,6 +346,12 @@ public class DownloadManagerService extends Service { mManager.mPrefMeteredDownloads = prefs.getBoolean(key, false); } else if (key.equals(getString(R.string.downloads_queue_limit))) { mManager.mPrefQueueLimit = prefs.getBoolean(key, true); + } else if (key.equals(getString(R.string.downloads_storage_api))) { + setupStorageAPI(false); + } else if (key.equals(getString(R.string.download_path_video_key))) { + loadMainStorage(key, DownloadManager.TAG_VIDEO, false); + } else if (key.equals(getString(R.string.download_path_audio_key))) { + loadMainStorage(key, DownloadManager.TAG_AUDIO, false); } } @@ -370,43 +369,61 @@ public class DownloadManagerService extends Service { mForeground = state; } - public static void startMission(Context context, String urls[], String location, String name, char kind, + /** + * Start a new download mission + * + * @param context the activity context + * @param urls the list of urls to download + * @param storage where the file is saved + * @param kind type of file (a: audio v: video s: subtitle ?: file-extension defined) + * @param threads the number of threads maximal used to download chunks of the file. + * @param psName the name of the required post-processing algorithm, or {@code null} to ignore. + * @param source source url of the resource + * @param psArgs the arguments for the post-processing algorithm. + * @param nearLength the approximated final length of the file + */ + public static void startMission(Context context, String urls[], StoredFileHelper storage, char kind, int threads, String source, String psName, String[] psArgs, long nearLength) { Intent intent = new Intent(context, DownloadManagerService.class); intent.setAction(Intent.ACTION_RUN); intent.putExtra(EXTRA_URLS, urls); - intent.putExtra(EXTRA_NAME, name); - intent.putExtra(EXTRA_LOCATION, location); + intent.putExtra(EXTRA_PATH, storage.getUri()); intent.putExtra(EXTRA_KIND, kind); intent.putExtra(EXTRA_THREADS, threads); intent.putExtra(EXTRA_SOURCE, source); intent.putExtra(EXTRA_POSTPROCESSING_NAME, psName); intent.putExtra(EXTRA_POSTPROCESSING_ARGS, psArgs); intent.putExtra(EXTRA_NEAR_LENGTH, nearLength); + intent.putExtra(EXTRA_MAIN_STORAGE_TAG, storage.getTag()); context.startService(intent); } - public static void checkForRunningMission(Context context, String location, String name, DMChecker checker) { - Intent intent = new Intent(); - intent.setClass(context, DownloadManagerService.class); - context.startService(intent); + public void startMission(Intent intent) { + String[] urls = intent.getStringArrayExtra(EXTRA_URLS); + Uri path = intent.getParcelableExtra(EXTRA_PATH); + int threads = intent.getIntExtra(EXTRA_THREADS, 1); + char kind = intent.getCharExtra(EXTRA_KIND, '?'); + String psName = intent.getStringExtra(EXTRA_POSTPROCESSING_NAME); + String[] psArgs = intent.getStringArrayExtra(EXTRA_POSTPROCESSING_ARGS); + String source = intent.getStringExtra(EXTRA_SOURCE); + long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0); + String tag = intent.getStringExtra(EXTRA_MAIN_STORAGE_TAG); - context.bindService(intent, new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName cname, IBinder service) { - try { - ((DMBinder) service).getDownloadManager().checkForRunningMission(location, name, checker); - } catch (Exception err) { - Log.w(TAG, "checkForRunningMission() callback is defective", err); - } + StoredFileHelper storage; + try { + storage = new StoredFileHelper(this, path, tag); + } catch (IOException e) { + throw new RuntimeException(e);// this never should happen + } - context.unbindService(this); - } + final DownloadMission mission = new DownloadMission(urls, storage, kind, Postprocessing.getAlgorithm(psName, psArgs)); + mission.threadCount = threads; + mission.source = source; + mission.nearLength = nearLength; - @Override - public void onServiceDisconnected(ComponentName name) { - } - }, Context.BIND_AUTO_CREATE); + handleConnectivityState(true);// first check the actual network status + + mManager.startMission(mission); } public void notifyFinishedDownload(String name) { @@ -471,12 +488,12 @@ public class DownloadManagerService extends Service { if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { downloadFailedNotification.setContentTitle(getString(R.string.app_name)); downloadFailedNotification.setStyle(new NotificationCompat.BigTextStyle() - .bigText(getString(R.string.download_failed).concat(": ").concat(mission.name))); + .bigText(getString(R.string.download_failed).concat(": ").concat(mission.storage.getName()))); } else { downloadFailedNotification.setContentTitle(getString(R.string.download_failed)); - downloadFailedNotification.setContentText(mission.name); + downloadFailedNotification.setContentText(mission.storage.getName()); downloadFailedNotification.setStyle(new NotificationCompat.BigTextStyle() - .bigText(mission.name)); + .bigText(mission.storage.getName())); } mNotificationManager.notify(id, downloadFailedNotification.build()); @@ -508,16 +525,81 @@ public class DownloadManagerService extends Service { mLockAcquired = acquire; } + private void setupStorageAPI(boolean acquire) { + loadMainStorage(getString(R.string.download_path_audio_key), DownloadManager.TAG_VIDEO, acquire); + loadMainStorage(getString(R.string.download_path_video_key), DownloadManager.TAG_AUDIO, acquire); + } + + void loadMainStorage(String prefKey, String tag, boolean acquire) { + String path = mPrefs.getString(prefKey, null); + + final String JAVA_IO = getString(R.string.downloads_storage_api_default); + boolean useJavaIO = JAVA_IO.equals(mPrefs.getString(getString(R.string.downloads_storage_api), JAVA_IO)); + + final String defaultPath; + if (tag.equals(DownloadManager.TAG_VIDEO)) + defaultPath = Environment.DIRECTORY_MOVIES; + else// if (tag.equals(DownloadManager.TAG_AUDIO)) + defaultPath = Environment.DIRECTORY_MUSIC; + + StoredDirectoryHelper mainStorage; + if (path == null || path.isEmpty()) { + mainStorage = useJavaIO ? new StoredDirectoryHelper(defaultPath, tag) : null; + } else { + + if (path.charAt(0) == File.separatorChar) { + Log.i(TAG, "Migrating old save path: " + path); + + useJavaIO = true; + path = Uri.fromFile(new File(path)).toString(); + + mPrefs.edit().putString(prefKey, path).apply(); + } + + if (useJavaIO) { + mainStorage = new StoredDirectoryHelper(path, tag); + } else { + + // tree api is not available in older versions + if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + mainStorage = null; + } else { + try { + mainStorage = new StoredDirectoryHelper(this, Uri.parse(path), tag); + if (acquire) mainStorage.acquirePermissions(); + } catch (IOException e) { + Log.e(TAG, "Failed to load the storage of " + tag + " from path: " + path, e); + mainStorage = null; + } + } + } + } + + if (tag.equals(DownloadManager.TAG_VIDEO)) + mManager.mMainStorageVideo = mainStorage; + else// if (tag.equals(DownloadManager.TAG_AUDIO)) + mManager.mMainStorageAudio = mainStorage; + } //////////////////////////////////////////////////////////////////////////////////////////////// // Wrappers for DownloadManager //////////////////////////////////////////////////////////////////////////////////////////////// - public class DMBinder extends Binder { + public class DownloadManagerBinder extends Binder { public DownloadManager getDownloadManager() { return mManager; } + @Nullable + public StoredDirectoryHelper getMainStorageVideo() { + return mManager.mMainStorageVideo; + } + + @Nullable + public StoredDirectoryHelper getMainStorageAudio() { + return mManager.mMainStorageAudio; + } + public void addMissionEventListener(Handler handler) { manageObservers(handler, true); } @@ -548,10 +630,4 @@ public class DownloadManagerService extends Service { } - public interface DMChecker { - void callback(MissionCheck result); - } - - public enum MissionCheck {None, Pending, PendingRunning, Finished} - } diff --git a/app/src/main/java/us/shandian/giga/service/MissionState.java b/app/src/main/java/us/shandian/giga/service/MissionState.java new file mode 100644 index 000000000..2d7802ff5 --- /dev/null +++ b/app/src/main/java/us/shandian/giga/service/MissionState.java @@ -0,0 +1,5 @@ +package us.shandian.giga.service; + +public enum MissionState { + None, Pending, PendingRunning, Finished +} diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java index cada3aeb8..4d80588e0 100644 --- a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java +++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java @@ -8,7 +8,6 @@ import android.content.Intent; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; -import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Message; @@ -49,6 +48,7 @@ import java.util.Collections; import us.shandian.giga.get.DownloadMission; import us.shandian.giga.get.FinishedMission; import us.shandian.giga.get.Mission; +import us.shandian.giga.io.StoredFileHelper; import us.shandian.giga.service.DownloadManager; import us.shandian.giga.service.DownloadManagerService; import us.shandian.giga.ui.common.Deleter; @@ -69,6 +69,7 @@ import static us.shandian.giga.get.DownloadMission.ERROR_PERMISSION_DENIED; import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING; import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD; import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_STOPPED; +import static us.shandian.giga.get.DownloadMission.ERROR_PROGRESS_LOST; import static us.shandian.giga.get.DownloadMission.ERROR_SSL_EXCEPTION; import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION; import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_HOST; @@ -97,8 +98,9 @@ public class MissionAdapter extends Adapter { private MenuItem mStartButton; private MenuItem mPauseButton; private View mEmptyMessage; + private RecoverHelper mRecover; - public MissionAdapter(Context context, DownloadManager downloadManager, View emptyMessage) { + public MissionAdapter(Context context, @NonNull DownloadManager downloadManager, View emptyMessage) { mContext = context; mDownloadManager = downloadManager; mDeleter = null; @@ -156,7 +158,11 @@ public class MissionAdapter extends Adapter { if (h.item.mission instanceof DownloadMission) { mPendingDownloadsItems.remove(h); - if (mPendingDownloadsItems.size() < 1) setAutoRefresh(false); + if (mPendingDownloadsItems.size() < 1) { + setAutoRefresh(false); + if (mStartButton != null) mStartButton.setVisible(false); + if (mPauseButton != null) mPauseButton.setVisible(false); + } } h.popupMenu.dismiss(); @@ -189,10 +195,10 @@ public class MissionAdapter extends Adapter { ViewHolderItem h = (ViewHolderItem) view; h.item = item; - Utility.FileType type = Utility.getFileType(item.mission.kind, item.mission.name); + Utility.FileType type = Utility.getFileType(item.mission.kind, item.mission.storage.getName()); h.icon.setImageResource(Utility.getIconForFileType(type)); - h.name.setText(item.mission.name); + h.name.setText(item.mission.storage.getName()); h.progress.setColors(Utility.getBackgroundForFileType(mContext, type), Utility.getForegroundForFileType(mContext, type)); @@ -273,7 +279,7 @@ public class MissionAdapter extends Adapter { long length = mission.getLength(); int state; - if (mission.isPsFailed()) { + if (mission.isPsFailed() || mission.errCode == ERROR_POSTPROCESSING_HOLD) { state = 0; } else if (!mission.running) { state = mission.enqueued ? 1 : 2; @@ -334,11 +340,17 @@ public class MissionAdapter extends Adapter { if (BuildConfig.DEBUG) Log.v(TAG, "Mime: " + mimeType + " package: " + BuildConfig.APPLICATION_ID + ".provider"); - Uri uri = FileProvider.getUriForFile( - mContext, - BuildConfig.APPLICATION_ID + ".provider", - mission.getDownloadedFile() - ); + Uri uri; + + if (mission.storage.isDirect()) { + uri = FileProvider.getUriForFile( + mContext, + BuildConfig.APPLICATION_ID + ".provider", + mission.storage.getIOFile() + ); + } else { + uri = mission.storage.getUri(); + } Intent intent = new Intent(); intent.setAction(Intent.ACTION_VIEW); @@ -366,13 +378,13 @@ public class MissionAdapter extends Adapter { Intent intent = new Intent(Intent.ACTION_SEND); intent.setType(resolveMimeType(mission)); - intent.putExtra(Intent.EXTRA_STREAM, mission.getDownloadedFile().toURI()); + intent.putExtra(Intent.EXTRA_STREAM, mission.storage.getUri()); mContext.startActivity(Intent.createChooser(intent, null)); } private static String resolveMimeType(@NonNull Mission mission) { - String ext = Utility.getFileExt(mission.getDownloadedFile().getName()); + String ext = Utility.getFileExt(mission.storage.getName()); if (ext == null) return DEFAULT_MIME_TYPE; String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1)); @@ -381,7 +393,7 @@ public class MissionAdapter extends Adapter { } private boolean checkInvalidFile(@NonNull Mission mission) { - if (mission.getDownloadedFile().exists()) return false; + if (mission.storage.existsAsFile()) return false; Toast.makeText(mContext, R.string.missing_file, Toast.LENGTH_SHORT).show(); return true; @@ -462,6 +474,8 @@ public class MissionAdapter extends Adapter { case ERROR_UNKNOWN_EXCEPTION: showError(mission.errObject, UserAction.DOWNLOAD_FAILED, R.string.general_error); return; + case ERROR_PROGRESS_LOST: + msg = R.string.error_progress_lost; default: if (mission.errCode >= 100 && mission.errCode < 600) { msgEx = "HTTP " + mission.errCode; @@ -490,7 +504,7 @@ public class MissionAdapter extends Adapter { } builder.setNegativeButton(android.R.string.ok, (dialog, which) -> dialog.cancel()) - .setTitle(mission.name) + .setTitle(mission.storage.getName()) .create() .show(); } @@ -539,6 +553,10 @@ public class MissionAdapter extends Adapter { updateProgress(h); return true; case R.id.retry: + if (mission.hasInvalidStorage()) { + mRecover.tryRecover(mission); + return true; + } mission.psContinue(true); return true; case R.id.cancel: @@ -561,7 +579,7 @@ public class MissionAdapter extends Adapter { return true; case R.id.md5: case R.id.sha1: - new ChecksumTask(mContext).execute(h.item.mission.getDownloadedFile().getAbsolutePath(), ALGORITHMS.get(id)); + new ChecksumTask(mContext).execute(h.item.mission.storage, ALGORITHMS.get(id)); return true; case R.id.source: /*Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(h.item.mission.source)); @@ -641,19 +659,38 @@ public class MissionAdapter extends Adapter { } - public void deleterDispose(Bundle bundle) { - if (mDeleter != null) mDeleter.dispose(bundle); + public void deleterDispose(boolean commitChanges) { + if (mDeleter != null) mDeleter.dispose(commitChanges); } - public void deleterLoad(Bundle bundle, View view) { + public void deleterLoad(View view) { if (mDeleter == null) - mDeleter = new Deleter(bundle, view, mContext, this, mDownloadManager, mIterator, mHandler); + mDeleter = new Deleter(view, mContext, this, mDownloadManager, mIterator, mHandler); } public void deleterResume() { if (mDeleter != null) mDeleter.resume(); } + public void recoverMission(DownloadMission mission, StoredFileHelper newStorage) { + for (ViewHolderItem h : mPendingDownloadsItems) { + if (mission != h.item.mission) continue; + + mission.changeStorage(newStorage); + mission.errCode = DownloadMission.ERROR_NOTHING; + mission.errObject = null; + + h.status.setText(UNDEFINED_PROGRESS); + h.state = -1; + h.size.setText(Utility.formatBytes(mission.getLength())); + h.progress.setMarquee(true); + + mDownloadManager.resumeMission(mission); + return; + } + + } + private boolean mUpdaterRunning = false; private final Runnable rUpdater = this::updater; @@ -695,6 +732,10 @@ public class MissionAdapter extends Adapter { return Float.isNaN(value) || Float.isInfinite(value); } + public void setRecover(@NonNull RecoverHelper callback) { + mRecover = callback; + } + class ViewHolderItem extends RecyclerView.ViewHolder { DownloadManager.MissionItem item; @@ -780,7 +821,11 @@ public class MissionAdapter extends Adapter { DownloadMission mission = item.mission instanceof DownloadMission ? (DownloadMission) item.mission : null; if (mission != null) { - if (mission.isPsRunning()) { + if (mission.hasInvalidStorage()) { + retry.setEnabled(true); + delete.setEnabled(true); + showError.setEnabled(true); + } else if (mission.isPsRunning()) { switch (mission.errCode) { case ERROR_INSUFFICIENT_STORAGE: case ERROR_POSTPROCESSING_HOLD: @@ -838,7 +883,7 @@ public class MissionAdapter extends Adapter { } - static class ChecksumTask extends AsyncTask { + static class ChecksumTask extends AsyncTask { ProgressDialog progressDialog; WeakReference weakReference; @@ -861,8 +906,8 @@ public class MissionAdapter extends Adapter { } @Override - protected String doInBackground(String... params) { - return Utility.checksum(params[0], params[1]); + protected String doInBackground(Object... params) { + return Utility.checksum((StoredFileHelper) params[0], (String) params[1]); } @Override @@ -889,4 +934,8 @@ public class MissionAdapter extends Adapter { } } + public interface RecoverHelper { + void tryRecover(DownloadMission mission); + } + } diff --git a/app/src/main/java/us/shandian/giga/ui/common/Deleter.java b/app/src/main/java/us/shandian/giga/ui/common/Deleter.java index 6407ab019..573bead94 100644 --- a/app/src/main/java/us/shandian/giga/ui/common/Deleter.java +++ b/app/src/main/java/us/shandian/giga/ui/common/Deleter.java @@ -3,8 +3,6 @@ package us.shandian.giga.ui.common; import android.content.Context; import android.content.Intent; import android.graphics.Color; -import android.net.Uri; -import android.os.Bundle; import android.os.Handler; import android.support.design.widget.Snackbar; import android.view.View; @@ -23,8 +21,6 @@ public class Deleter { private static final int TIMEOUT = 5000;// ms private static final int DELAY = 350;// ms private static final int DELAY_RESUME = 400;// ms - private static final String BUNDLE_NAMES = "us.shandian.giga.ui.common.deleter.names"; - private static final String BUNDLE_LOCATIONS = "us.shandian.giga.ui.common.deleter.locations"; private Snackbar snackbar; private ArrayList items; @@ -41,7 +37,7 @@ public class Deleter { private final Runnable rNext; private final Runnable rCommit; - public Deleter(Bundle b, View v, Context c, MissionAdapter a, DownloadManager d, MissionIterator i, Handler h) { + public Deleter(View v, Context c, MissionAdapter a, DownloadManager d, MissionIterator i, Handler h) { mView = v; mContext = c; mAdapter = a; @@ -55,27 +51,6 @@ public class Deleter { rCommit = this::commit; items = new ArrayList<>(2); - - if (b != null) { - String[] names = b.getStringArray(BUNDLE_NAMES); - String[] locations = b.getStringArray(BUNDLE_LOCATIONS); - - if (names == null || locations == null) return; - if (names.length < 1 || locations.length < 1) return; - if (names.length != locations.length) return; - - items.ensureCapacity(names.length); - - for (int j = 0; j < locations.length; j++) { - Mission mission = mDownloadManager.getAnyMission(locations[j], names[j]); - if (mission == null) continue; - - items.add(mission); - mIterator.hide(mission); - } - - if (items.size() > 0) resume(); - } } public void append(Mission item) { @@ -104,7 +79,7 @@ public class Deleter { private void next() { if (items.size() < 1) return; - String msg = mContext.getString(R.string.file_deleted).concat(":\n").concat(items.get(0).name); + String msg = mContext.getString(R.string.file_deleted).concat(":\n").concat(items.get(0).storage.getName()); snackbar = Snackbar.make(mView, msg, Snackbar.LENGTH_INDEFINITE); snackbar.setAction(R.string.undo, s -> forget()); @@ -125,7 +100,7 @@ public class Deleter { mDownloadManager.deleteMission(mission); if (mission instanceof FinishedMission) { - mContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(mission.getDownloadedFile()))); + mContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, mission.storage.getUri())); } break; } @@ -151,27 +126,14 @@ public class Deleter { mHandler.postDelayed(rShow, DELAY_RESUME); } - public void dispose(Bundle bundle) { + public void dispose(boolean commitChanges) { if (items.size() < 1) return; pause(); - if (bundle == null) { - for (Mission mission : items) mDownloadManager.deleteMission(mission); - items = null; - return; - } + if (!commitChanges) return; - String[] names = new String[items.size()]; - String[] locations = new String[items.size()]; - - for (int i = 0; i < items.size(); i++) { - Mission mission = items.get(i); - names[i] = mission.name; - locations[i] = mission.location; - } - - bundle.putStringArray(BUNDLE_NAMES, names); - bundle.putStringArray(BUNDLE_LOCATIONS, locations); + for (Mission mission : items) mDownloadManager.deleteMission(mission); + items = null; } } diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java index a3786a5e6..82ab777b0 100644 --- a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java +++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java @@ -1,7 +1,6 @@ package us.shandian.giga.ui.fragment; import android.app.Activity; -import android.app.Fragment; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -10,6 +9,7 @@ import android.content.SharedPreferences; import android.os.Bundle; import android.os.IBinder; import android.preference.PreferenceManager; +import android.support.v4.app.Fragment; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; @@ -18,18 +18,24 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.Toast; import org.schabi.newpipe.R; import org.schabi.newpipe.util.ThemeHelper; +import java.io.IOException; + +import us.shandian.giga.get.DownloadMission; +import us.shandian.giga.io.StoredFileHelper; import us.shandian.giga.service.DownloadManager; import us.shandian.giga.service.DownloadManagerService; -import us.shandian.giga.service.DownloadManagerService.DMBinder; +import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder; import us.shandian.giga.ui.adapter.MissionAdapter; public class MissionsFragment extends Fragment { private static final int SPAN_SIZE = 2; + private static final int REQUEST_DOWNLOAD_PATH_SAF = 0x1230; private SharedPreferences mPrefs; private boolean mLinear; @@ -45,24 +51,32 @@ public class MissionsFragment extends Fragment { private LinearLayoutManager mLinearManager; private Context mContext; - private DMBinder mBinder; - private Bundle mBundle; + private DownloadManagerBinder mBinder; private boolean mForceUpdate; + private DownloadMission unsafeMissionTarget = null; + private ServiceConnection mConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder binder) { - mBinder = (DownloadManagerService.DMBinder) binder; + mBinder = (DownloadManagerBinder) binder; mBinder.clearDownloadNotifications(); mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty); - mAdapter.deleterLoad(mBundle, getView()); + mAdapter.deleterLoad(getView()); + + mAdapter.setRecover(mission -> + StoredFileHelper.requestSafWithFileCreation( + MissionsFragment.this, + REQUEST_DOWNLOAD_PATH_SAF, + mission.storage.getName(), + mission.storage.getType() + ) + ); setAdapterButtons(); - mBundle = null; - mBinder.addMissionEventListener(mAdapter.getMessenger()); mBinder.enableNotifications(false); @@ -84,9 +98,6 @@ public class MissionsFragment extends Fragment { mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); mLinear = mPrefs.getBoolean("linear", false); - //mContext = getActivity().getApplicationContext(); - mBundle = savedInstanceState; - // Bind the service mContext.bindService(new Intent(mContext, DownloadManagerService.class), mConnection, Context.BIND_AUTO_CREATE); @@ -148,7 +159,7 @@ public class MissionsFragment extends Fragment { mBinder.removeMissionEventListener(mAdapter.getMessenger()); mBinder.enableNotifications(true); mContext.unbindService(mConnection); - mAdapter.deleterDispose(null); + mAdapter.deleterDispose(true); mBinder = null; mAdapter = null; @@ -178,10 +189,12 @@ public class MissionsFragment extends Fragment { return true; case R.id.start_downloads: item.setVisible(false); + mPause.setVisible(true); mBinder.getDownloadManager().startAllMissions(); return true; case R.id.pause_downloads: item.setVisible(false); + mStart.setVisible(true); mBinder.getDownloadManager().pauseAllMissions(false); mAdapter.ensurePausedMissions();// update items view default: @@ -231,7 +244,7 @@ public class MissionsFragment extends Fragment { super.onSaveInstanceState(outState); if (mAdapter != null) { - mAdapter.deleterDispose(outState); + mAdapter.deleterDispose(false); mForceUpdate = true; mBinder.removeMissionEventListener(mAdapter.getMessenger()); } @@ -260,4 +273,22 @@ public class MissionsFragment extends Fragment { if (mAdapter != null) mAdapter.onPaused(); if (mBinder != null) mBinder.enableNotifications(true); } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (requestCode != REQUEST_DOWNLOAD_PATH_SAF || resultCode != Activity.RESULT_OK) return; + + if (unsafeMissionTarget == null || data.getData() == null) { + return;// unsafeMissionTarget cannot be null + } + + try { + StoredFileHelper storage = new StoredFileHelper(mContext, data.getData(), unsafeMissionTarget.storage.getTag()); + mAdapter.recoverMission(unsafeMissionTarget, storage); + } catch (IOException e) { + Toast.makeText(mContext, R.string.general_error, Toast.LENGTH_LONG).show(); + } + } } diff --git a/app/src/main/java/us/shandian/giga/util/Utility.java b/app/src/main/java/us/shandian/giga/util/Utility.java index e5149cf9b..d77e598d8 100644 --- a/app/src/main/java/us/shandian/giga/util/Utility.java +++ b/app/src/main/java/us/shandian/giga/util/Utility.java @@ -12,11 +12,11 @@ import android.support.v4.content.ContextCompat; import android.widget.Toast; import org.schabi.newpipe.R; +import org.schabi.newpipe.streams.io.SharpStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; @@ -25,7 +25,8 @@ import java.io.Serializable; import java.net.HttpURLConnection; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.util.Locale; + +import us.shandian.giga.io.StoredFileHelper; public class Utility { @@ -206,7 +207,7 @@ public class Utility { Toast.makeText(context, R.string.msg_copied, Toast.LENGTH_SHORT).show(); } - public static String checksum(String path, String algorithm) { + public static String checksum(StoredFileHelper source, String algorithm) { MessageDigest md; try { @@ -215,11 +216,11 @@ public class Utility { throw new RuntimeException(e); } - FileInputStream i; + SharpStream i; try { - i = new FileInputStream(path); - } catch (FileNotFoundException e) { + i = source.getStream(); + } catch (Exception e) { throw new RuntimeException(e); } @@ -247,15 +248,15 @@ public class Utility { } @SuppressWarnings("ResultOfMethodCallIgnored") - public static boolean mkdir(File path, boolean allDirs) { - if (path.exists()) return true; + public static boolean mkdir(File p, boolean allDirs) { + if (p.exists()) return true; if (allDirs) - path.mkdirs(); + p.mkdirs(); else - path.mkdir(); + p.mkdir(); - return path.exists(); + return p.exists(); } public static long getContentLength(HttpURLConnection connection) { diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index b7a0ec7b0..dbf015c87 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -462,12 +462,12 @@ %s أنتهى التحميل إنشاء اسم فريد الكتابة فوق - يوجد ملف تحميل بهذا الاسم موجود مسبقاً + يوجد ملف تحميل بهذا الاسم موجود مسبقاً هنالك تحميل قيد التقدم بهذا الاسم إظهار خطأ كود - لا يمكن إنشاء الملف - لا يمكن إنشاء المجلد الوجهة + لا يمكن إنشاء الملف + لا يمكن إنشاء المجلد الوجهة تم رفضها من قبل النظام فشل اتصال الأمن تعذر العثور على الخادم diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 4285a73dc..f606281f4 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -432,8 +432,8 @@ Genera un nom únic Mostra l\'error Codi - No es pot crear el fitxer - No es pot crear la carpeta de destinació + No es pot crear el fitxer + No es pot crear la carpeta de destinació Atura Esdeveniments Notificacions de noves versions del NewPipe diff --git a/app/src/main/res/values-cmn/strings.xml b/app/src/main/res/values-cmn/strings.xml index f6c8e3e4a..73eb43c36 100644 --- a/app/src/main/res/values-cmn/strings.xml +++ b/app/src/main/res/values-cmn/strings.xml @@ -437,11 +437,11 @@ %s已下载完毕 生成独特的名字 覆写 - 同名的已下载文件已经存在 + 同名的已下载文件已经存在 同名下载进行中 显示错误 代码 - 无法创建该文件 + 无法创建该文件 系统拒绝此批准 安全连接失败 找不到服务器 @@ -464,7 +464,7 @@ 网格 切换视图 NewPipe 更新可用! - 无法创建目标文件夹 + 无法创建目标文件夹 服务器不接受多线程下载, 请重试使用 @string/msg_threads = 1 请求范围无法满足 继续进行%s个待下载转移 diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 4dc81f9c2..92535310e 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -372,8 +372,8 @@ Der er en download i gang med dette navn Vis fejl Kode - Filen kan ikke oprettes - Destinationsmappen kan ikke oprettes + Filen kan ikke oprettes + Destinationsmappen kan ikke oprettes Adgang nægtet af systemet Sikker forbindelse fejlede Kunne ikke finde serveren diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index dada4e80f..ef2789846 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -448,12 +448,12 @@ %s heruntergeladen Eindeutigen Namen erzeugen Überschreiben - Eine heruntergeladene Datei dieses Namens existiert bereits + Eine heruntergeladene Datei dieses Namens existiert bereits Eine Datei dieses Namens wird gerade heruntergeladen Fehler anzeigen Code - Die Datei kann nicht erstellt werden - Der Zielordner kann nicht erstellt werden + Die Datei kann nicht erstellt werden + Der Zielordner kann nicht erstellt werden System verweigert den Zugriff Sichere Verbindung fehlgeschlagen Der Server konnte nicht gefunden werden diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 818efc74c..eee110474 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -14,7 +14,7 @@ Compartir con Elegir navegador rotación - Ruta de descarga de vídeo + Carpeta de descarga de vídeo Ruta para almacenar los vídeos descargados Introducir directorio de descargas para vídeos Resolución por defecto de vídeo @@ -40,7 +40,7 @@ (Experimental) Forzar la descarga a través de Tor para una mayor privacidad (transmisión de vídeos aún no compatible). No se puede crear la carpeta de descarga \'%1$s\' Carpeta de descarga creada \'%1$s\' - Los audios descargados se almacenan aquí + Ruta para almacenar los audios descargados Introducir ruta de descarga para archivos de audio Bloqueado por GEMA Carpeta de descarga de audio @@ -418,7 +418,9 @@ abrir en modo popup Generar nombre único Sobrescribir - Ya existe un archivo descargado con este nombre + Ya existe un archivo con este nombre + Ya existe un archivo descargado con este nombre + No se puede sobrescribir el archivo Hay una descarga en curso con este nombre Hay una descarga pendiente con este nombre @@ -440,8 +442,8 @@ abrir en modo popup Mostrar error Codigo - No se puede crear la carpeta de destino - No se puede crear el archivo + No se puede crear la carpeta de destino + No se puede crear el archivo Permiso denegado por el sistema Fallo la conexión segura No se pudo encontrar el servidor @@ -453,6 +455,19 @@ abrir en modo popup Fallo el post-procesado NewPipe se cerro mientras se trabajaba en el archivo No hay suficiente espacio disponible en el dispositivo + Se perdió el progreso porque el archivo fue eliminado + + API de almacenamiento + Seleccione que API utilizar para almacenar las descargas + + Framework de acceso a almacenamiento + Java I/O + + Guardar como… + + No es posible descargar a una tarjeta SD externa. \¿Restablecer la ubicación de la carpeta de descarga\? + + Seleccione los directorios de descarga Desuscribirse Nueva pestaña diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 095bbfe5e..2868528e9 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -446,12 +446,12 @@ %s deskarga amaituta Sortu izen bakana Gainidatzi - Badago izen bera duen deskargatutako fitxategi bat + Badago izen bera duen deskargatutako fitxategi bat Badago izen bera duen deskarga bat abian Erakutsi errorea Kodea - Ezin da fitxategia sortu - Ezin da helburu karpeta sortu + Ezin da fitxategia sortu + Ezin da helburu karpeta sortu Sistemak baimena ukatu du Konexio seguruak huts egin du Ezin izan da zerbitzaria aurkitu diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 9edee2411..41569ff0c 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -451,12 +451,12 @@ %s הורדות הסתיימו יצירת שם ייחודי שכתוב - כבר קיים קובץ בשם הזה + כבר קיים קובץ בשם הזה אחת ההורדות הפעילות כבר נושאת את השם הזה הצגת שגיאה קוד - לא ניתן ליצור את הקובץ - לא ניתן ליצור את תיקיית היעד + לא ניתן ליצור את הקובץ + לא ניתן ליצור את תיקיית היעד ההרשאה נדחתה על ידי המערכת החיבור המאובטח נכשל לא ניתן למצוא את השרת diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index 56f31eac6..31801434b 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -447,12 +447,12 @@ %s unduhan selesai Hasilkan nama unik Timpa - File yang diunduh dengan nama ini sudah ada + File yang diunduh dengan nama ini sudah ada Ada unduhan yang sedang berlangsung dengan nama ini Tunjukkan kesalahan Kode - File tidak dapat dibuat - Folder tujuan tidak dapat dibuat + File tidak dapat dibuat + Folder tujuan tidak dapat dibuat Izin ditolak oleh sistem Koneksi aman gagal Tidak dapat menemukan server diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 9bccc11df..4ff8de734 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -449,12 +449,12 @@ %s download finiti Genera un nome unico Sovrascrivi - Esiste già un file scaricato con lo stesso nome + Esiste già un file scaricato con lo stesso nome C\'è un download in progresso con questo nome Mostra errore Codice - Impossibile creare il file - Impossibile creare la cartella di destinazione + Impossibile creare il file + Impossibile creare la cartella di destinazione Permesso negato dal sistema Connessione sicura fallita Impossibile trovare il server diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 124c3580b..78a20b1ab 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -365,10 +365,10 @@ 自動生成 アプリの再起動後、設定した字幕設定が反映されます 何もありません - 保存したエクスポートファイルからYouTubeの購読をインポート: -\n -\n1. このURLを開きます: %1$s -\n2. ログインしていなければログインします + 保存したエクスポートファイルからYouTubeの購読をインポート: +\n +\n1. このURLを開きます: %1$s +\n2. ログインしていなければログインします \n3. ダウンロードが始まります (これがエクスポートファイルです) リセット 同意する @@ -391,8 +391,8 @@ \n3. 必要に応じてログインします \n4. リダイレクトされたプロファイル URL をコピーします。 あなたのID, soundcloud.com/あなたのid - この操作により通信料金が増えることがあります。ご注意ください。 -\n + この操作により通信料金が増えることがあります。ご注意ください。 +\n \n続行しますか\? 再生速度を変更 速度と音程を連動せずに変更 (歪むかもしれません) diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index 7c8b56bec..ced8235f7 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -443,12 +443,12 @@ %s muat turun selesai Menjana nama yang unik Timpa - Fail yang dimuat turun dengan nama ini sudah wujud + Fail yang dimuat turun dengan nama ini sudah wujud Terdapat muat turun yang sedang berjalan dengan nama ini Tunjukkan kesilapan Kod - Fail tidak boleh dibuat - Folder destinasi tidak boleh dibuat + Fail tidak boleh dibuat + Folder destinasi tidak boleh dibuat Kebenaran ditolak oleh sistem Sambungan selamat gagal Tidak dapat mencari server diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index a806df31a..2f5d19c67 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -526,12 +526,12 @@ %s nedlastinger fullført Generer unikt navn Overskriv - Nedlastet fil ved dette navnet finnes allerede + Nedlastet fil ved dette navnet finnes allerede Nedlasting med dette navnet underveis allerede Vis feil Kode - Filen kan ikke opprettes - Målmappen kan ikke opprettes + Filen kan ikke opprettes + Målmappen kan ikke opprettes Tilgang nektet av systemet Sikker tilkobling mislyktes Fant ikke tjeneren diff --git a/app/src/main/res/values-nl-rBE/strings.xml b/app/src/main/res/values-nl-rBE/strings.xml index 51a30aa36..44b2ef6ab 100644 --- a/app/src/main/res/values-nl-rBE/strings.xml +++ b/app/src/main/res/values-nl-rBE/strings.xml @@ -445,12 +445,12 @@ %s downloads voltooid Unieke naam genereren Overschrijven - Der bestaat al een gedownload bestand met deze naam + Der bestaat al een gedownload bestand met deze naam Der is al een download met deze naam bezig Foutmelding weergeven Code - Het bestand kan niet aangemaakt worden - De doelmap kan niet aangemaakt worden + Het bestand kan niet aangemaakt worden + De doelmap kan niet aangemaakt worden Toelating geweigerd door het systeem Beveiligde verbinding is mislukt Kon de server niet vinden diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 04b47c4c8..96de68b57 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -449,12 +449,12 @@ %s downloads voltooid Genereer een unieke naam Overschrijven - Er bestaat al een gedownload bestand met deze naam + Er bestaat al een gedownload bestand met deze naam Er is een download aan de gang met deze naam Toon foutmelding Code - Het bestand kan niet worden gemaakt - De doelmap kan niet worden gemaakt + Het bestand kan niet worden gemaakt + De doelmap kan niet worden gemaakt Toestemming door het systeem geweigerd Beveiligde connectie is mislukt Kon de server niet vinden diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 4fedc93d9..29070990f 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -446,12 +446,12 @@ %s pobieranie zostało zakończone Wygeneruj unikalną nazwę Zastąp - Pobrany plik o tej nazwie już istnieje + Pobrany plik o tej nazwie już istnieje Trwa pobieranie z tą nazwą Pokaż błąd Kod - Nie można utworzyć pliku - Nie można utworzyć folderu docelowego + Nie można utworzyć pliku + Nie można utworzyć folderu docelowego Odmowa dostępu do systemu Bezpieczne połączenie nie powiodło się Nie można znaleźć serwera diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 798131f37..8a16b752d 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -446,12 +446,12 @@ abrir em modo popup %s downloads terminados Gerar nome único "Sobrescrever " - Um arquivo baixado com esse nome já existe + Um arquivo baixado com esse nome já existe Existe um download em progresso com esse nome Mostrar erro Código - O arquivo não pode ser criado - A pasta de destino não pode ser criada + O arquivo não pode ser criado + A pasta de destino não pode ser criada Permissão negada pelo sistema "Falha na conexão segura " Não foi possível encontrar o servidor diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 9db2e401a..a86c5b809 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -442,12 +442,12 @@ %s descargas terminadas Gerar nome único Sobrescrever - Um ficheiro descarregado com este nome já existe + Um ficheiro descarregado com este nome já existe Já existe uma descarga em curso com este nome Mostrar erro Código - O ficheiro não pode ser criado - A pasta de destino não pode ser criada + O ficheiro não pode ser criado + A pasta de destino não pode ser criada Permissão negada pelo sistema Ligação segura falhou Não foi possível encontrar o servidor diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index d4bf5d803..620ca5619 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -442,12 +442,12 @@ Действие запрещено системой Ошибка загрузки Перезаписать - Файл с таким именем уже существует + Файл с таким именем уже существует Загрузка с таким именем уже выполняется Показать текст ошибки Код - Файл не может быть создан - Папка назначения не может быть создана + Папка назначения не может быть создана + Файл не может быть создан Доступ запрещен системой Сервер не найден "Сервер не поддерживает многопотоковую загрузку, попробуйте с @string/msg_threads = 1" diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 5209fe250..e518a1c0f 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -449,12 +449,12 @@ %s indirme bitti Benzersiz ad oluştur Üzerine yaz - Bu ada sahip indirilen bir dosya zaten var + Bu ada sahip indirilen bir dosya zaten var Bu ad ile devam eden bir indirme var Hatayı göster Kod - Dosya oluşturulamıyor - Hedef klasör oluşturulamıyor + Dosya oluşturulamıyor + Hedef klasör oluşturulamıyor İzin sistem tarafından reddedildi Güvenli bağlantı başarısız Sunucu bulunamadı diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index b1899622f..ff247c579 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -440,11 +440,11 @@ %s tải về đã xong Tạo tên riêng biệt Ghi đè - Có một tệp đã tải về trùng tên + Có một tệp đã tải về trùng tên Có một tệp trùng tên đang tải về Hiện lỗi - Không thể tạo tệp - Không thể tạo thư mục đích + Không thể tạo tệp + Không thể tạo thư mục đích Quyền bị từ chối bởi hệ thống Không thể tạo kết nối an toàn Không thể tìm máy chủ diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 441061657..0194418cf 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -445,12 +445,12 @@ %s 個下載已結束 生成獨特的名稱 覆寫 - 已有此名稱的已下載檔案 + 已有此名稱的已下載檔案 已有此名稱的當案的下載正在進行 顯示錯誤 代碼 - 無法建立檔案 - 無法建立目的地資料夾 + 無法建立檔案 + 無法建立目的地資料夾 被系統拒絕的權限 安全連線失敗 找不到伺服器 diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 214a074c4..b2be3135b 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -11,7 +11,7 @@ saved_tabs_key - download_path + download_path download_path_audio use_external_video_player @@ -160,6 +160,21 @@ clear_play_history clear_search_history + downloads_storage_api + + + javaIO + + + SAF + javaIO + + + + @string/storage_access_framework_description + @string/java_io_description + + file_rename file_replacement_character diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index afc6afeb3..7907d2974 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -176,7 +176,7 @@ Error External storage unavailable - Downloading to external SD card not yet possible. Reset download folder location\? + Downloading to external SD card not possible. Reset download folder location\? Network error Could not load all thumbnails Could not decrypt video URL signature @@ -512,15 +512,17 @@ Generate unique name Overwrite - A downloaded file with this name already exists + A file with this name already exists + A downloaded file with this name already exists + cannot overwrite the file There is a download in progress with this name There is a pending download with this name Show error Code - The file can not be created - The destination folder can not be created + The file can not be created + The destination folder can not be created Permission denied by the system Secure connection failed Could not find the server @@ -532,6 +534,7 @@ Post-processing failed NewPipe was closed while working on the file No space left on device + Progress lost, because the file was deleted Clear finished downloads Continue your %s pending transfers from Downloads @@ -546,4 +549,14 @@ Start downloads Pause downloads + Storage API + Select which API use to store the downloads + + Storage Access Framework + Java I/O + + Save as… + + Select the downloads save path + \ No newline at end of file diff --git a/app/src/main/res/xml/download_settings.xml b/app/src/main/res/xml/download_settings.xml index 9f32e7f2f..bbb91acac 100644 --- a/app/src/main/res/xml/download_settings.xml +++ b/app/src/main/res/xml/download_settings.xml @@ -4,10 +4,26 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:title="@string/settings_category_downloads_title"> + + + + + From d00dc798f468cf1e9f47ad8703b866259ebfdd32 Mon Sep 17 00:00:00 2001 From: kapodamy Date: Tue, 9 Apr 2019 18:38:34 -0300 Subject: [PATCH 129/138] more SAF implementation * full support for Directory API (Android Lollipop or later) * best effort to handle any kind errors (missing file, revoked permissions, etc) and recover the download * implemented directory choosing * fix download database version upgrading * misc. cleanup * do not release permission on the old save path (if the user change the download directory) under SAF api --- .../newpipe/download/DownloadDialog.java | 202 ++++++++------ .../settings/DownloadSettingsFragment.java | 164 +++++++---- .../schabi/newpipe/streams/DataReader.java | 4 +- .../newpipe/streams/Mp4FromDashWriter.java | 8 +- .../schabi/newpipe/util/FilenameUtils.java | 26 +- .../giga/get/DownloadInitializer.java | 2 +- .../us/shandian/giga/get/DownloadMission.java | 41 ++- .../shandian/giga/get/DownloadRunnable.java | 8 +- .../us/shandian/giga/get/FinishedMission.java | 2 + .../java/us/shandian/giga/get/Mission.java | 14 +- .../giga/get/sqlite/FinishedMissionStore.java | 44 ++- .../shandian/giga/io/CircularFileWriter.java | 91 ++++-- .../us/shandian/giga/io/FileStreamSAF.java | 5 + .../giga/io/StoredDirectoryHelper.java | 263 +++++++++++------- .../us/shandian/giga/io/StoredFileHelper.java | 248 +++++++++++------ .../giga/postprocessing/Mp4FromDashMuxer.java | 2 +- .../giga/postprocessing/Postprocessing.java | 15 +- .../giga/postprocessing/WebMMuxer.java | 2 +- .../giga/service/DownloadManager.java | 171 ++++++------ .../giga/service/DownloadManagerService.java | 134 +++++---- .../giga/ui/adapter/MissionAdapter.java | 33 ++- .../giga/ui/fragment/MissionsFragment.java | 29 +- .../java/us/shandian/giga/util/Utility.java | 2 + app/src/main/res/values-es/strings.xml | 4 +- app/src/main/res/values/settings_keys.xml | 12 +- app/src/main/res/values/strings.xml | 3 +- app/src/main/res/xml/download_settings.xml | 6 - assets/db.dia | Bin 2520 -> 2508 bytes 28 files changed, 946 insertions(+), 589 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index 4525c5988..8f4b569cd 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -15,12 +15,13 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.StringRes; import android.support.v4.app.DialogFragment; +import android.support.v4.provider.DocumentFile; import android.support.v7.app.AlertDialog; +import android.support.v7.view.menu.ActionMenuItemView; import android.support.v7.widget.Toolbar; import android.util.Log; import android.util.SparseArray; import android.view.LayoutInflater; -import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; @@ -177,9 +178,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck return; } - final Context context = getContext(); - if (context == null) - throw new RuntimeException("Context was null"); + context = getContext(); setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context)); Icepick.restoreInstanceState(this, savedInstanceState); @@ -321,11 +320,15 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck showFailedDialog(R.string.general_error); return; } - try { - continueSelectedDownload(new StoredFileHelper(getContext(), data.getData(), "")); - } catch (IOException e) { - showErrorActivity(e); + + DocumentFile docFile = DocumentFile.fromSingleUri(context, data.getData()); + if (docFile == null) { + showFailedDialog(R.string.general_error); + return; } + + // check if the selected file was previously used + checkSelectedDownload(null, data.getData(), docFile.getName(), docFile.getType()); } } @@ -337,14 +340,14 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck if (DEBUG) Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]"); boolean isLight = ThemeHelper.isLightThemeSelected(getActivity()); - okButton = toolbar.findViewById(R.id.okay); - okButton.setEnabled(false);// disable until the download service connection is done toolbar.setTitle(R.string.download_dialog_title); toolbar.setNavigationIcon(isLight ? R.drawable.ic_arrow_back_black_24dp : R.drawable.ic_arrow_back_white_24dp); toolbar.inflateMenu(R.menu.dialog_url); toolbar.setNavigationOnClickListener(v -> getDialog().dismiss()); + okButton = toolbar.findViewById(R.id.okay); + okButton.setEnabled(false);// disable until the download service connection is done toolbar.setOnMenuItemClickListener(item -> { if (item.getItemId() == R.id.okay) { @@ -504,15 +507,17 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck StoredDirectoryHelper mainStorageAudio = null; StoredDirectoryHelper mainStorageVideo = null; DownloadManager downloadManager = null; - - MenuItem okButton = null; + ActionMenuItemView okButton = null; + Context context; private String getNameEditText() { - return nameEditText.getText().toString().trim(); + String str = nameEditText.getText().toString().trim(); + + return FilenameUtils.createFilename(context, str.isEmpty() ? currentInfo.getName() : str); } private void showFailedDialog(@StringRes int msg) { - new AlertDialog.Builder(getContext()) + new AlertDialog.Builder(context) .setMessage(msg) .setNegativeButton(android.R.string.ok, null) .create() @@ -521,7 +526,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck private void showErrorActivity(Exception e) { ErrorActivity.reportError( - getContext(), + context, Collections.singletonList(e), null, null, @@ -530,18 +535,14 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck } private void prepareSelectedDownload() { - final Context context = getContext(); StoredDirectoryHelper mainStorage; MediaFormat format; String mime; // first, build the filename and get the output folder (if possible) + // later, run a very very very large file checking logic - String filename = getNameEditText() + "."; - if (filename.isEmpty()) { - filename = FilenameUtils.createFilename(context, currentInfo.getName()); - } - filename += "."; + String filename = getNameEditText().concat("."); switch (radioStreamsGroup.getCheckedRadioButtonId()) { case R.id.audio_button: @@ -567,34 +568,33 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck } if (mainStorage == null) { - // this part is called if... - // older android version running with SAF preferred - // save path not defined (via download settings) + // This part is called if with SAF preferred: + // * older android version running + // * save path not defined (via download settings) StoredFileHelper.requestSafWithFileCreation(this, REQUEST_DOWNLOAD_PATH_SAF, filename, mime); return; } // check for existing file with the same name - Uri result = mainStorage.findFile(filename); + checkSelectedDownload(mainStorage, mainStorage.findFile(filename), filename, mime); + } - if (result == null) { - // the file does not exists, create - StoredFileHelper storage = mainStorage.createFile(filename, mime); - if (storage == null || !storage.canWrite()) { - showFailedDialog(R.string.error_file_creation); - return; - } - - continueSelectedDownload(storage); - return; - } - - // the target filename is already use, try load + private void checkSelectedDownload(StoredDirectoryHelper mainStorage, Uri targetFile, String filename, String mime) { StoredFileHelper storage; + try { - storage = new StoredFileHelper(context, result, mime); - } catch (IOException e) { + if (mainStorage == null) { + // using SAF on older android version + storage = new StoredFileHelper(context, null, targetFile, ""); + } else if (targetFile == null) { + // the file does not exist, but it is probably used in a pending download + storage = new StoredFileHelper(mainStorage.getUri(), filename, mime, mainStorage.getTag()); + } else { + // the target filename is already use, attempt to use it + storage = new StoredFileHelper(context, mainStorage.getUri(), targetFile, mainStorage.getTag()); + } + } catch (Exception e) { showErrorActivity(e); return; } @@ -618,6 +618,25 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck msgBody = R.string.download_already_running; break; case None: + if (mainStorage == null) { + // This part is called if: + // * using SAF on older android version + // * save path not defined + continueSelectedDownload(storage); + return; + } else if (targetFile == null) { + // This part is called if: + // * the filename is not used in a pending/finished download + // * the file does not exists, create + storage = mainStorage.createFile(filename, mime); + if (storage == null || !storage.canWrite()) { + showFailedDialog(R.string.error_file_creation); + return; + } + + continueSelectedDownload(storage); + return; + } msgBtn = R.string.overwrite; msgBody = R.string.overwrite_unrelated_warning; break; @@ -625,49 +644,73 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck return; } - // handle user answer (overwrite or create another file with different name) - final String finalFilename = filename; - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(R.string.download_dialog_title) - .setMessage(msgBody) - .setPositiveButton(msgBtn, (dialog, which) -> { - dialog.dismiss(); - StoredFileHelper storageNew; - switch (state) { - case Finished: - case Pending: - downloadManager.forgetMission(storage); - case None: - // try take (or steal) the file permissions - try { - storageNew = new StoredFileHelper(context, result, mainStorage.getTag()); - if (storageNew.canWrite()) - continueSelectedDownload(storageNew); - else - showFailedDialog(R.string.error_file_creation); - } catch (IOException e) { - showErrorActivity(e); - } - break; - case PendingRunning: - // FIXME: createUniqueFile() is not tested properly - storageNew = mainStorage.createUniqueFile(finalFilename, mime); - if (storageNew == null) - showFailedDialog(R.string.error_file_creation); - else - continueSelectedDownload(storageNew); - break; + AlertDialog.Builder askDialog = new AlertDialog.Builder(context) + .setTitle(R.string.download_dialog_title) + .setMessage(msgBody) + .setNegativeButton(android.R.string.cancel, null); + final StoredFileHelper finalStorage = storage; + + + if (mainStorage == null) { + // This part is called if: + // * using SAF on older android version + // * save path not defined + switch (state) { + case Pending: + case Finished: + askDialog.setPositiveButton(msgBtn, (dialog, which) -> { + dialog.dismiss(); + downloadManager.forgetMission(finalStorage); + continueSelectedDownload(finalStorage); + }); + break; + } + + askDialog.create().show(); + return; + } + + askDialog.setPositiveButton(msgBtn, (dialog, which) -> { + dialog.dismiss(); + + StoredFileHelper storageNew; + switch (state) { + case Finished: + case Pending: + downloadManager.forgetMission(finalStorage); + case None: + if (targetFile == null) { + storageNew = mainStorage.createFile(filename, mime); + } else { + try { + // try take (or steal) the file + storageNew = new StoredFileHelper(context, mainStorage.getUri(), targetFile, mainStorage.getTag()); + } catch (IOException e) { + Log.e(TAG, "Failed to take (or steal) the file in " + targetFile.toString()); + storageNew = null; + } } - }) - .setNegativeButton(android.R.string.cancel, null) - .create() - .show(); + + if (storageNew != null && storageNew.canWrite()) + continueSelectedDownload(storageNew); + else + showFailedDialog(R.string.error_file_creation); + break; + case PendingRunning: + storageNew = mainStorage.createUniqueFile(filename, mime); + if (storageNew == null) + showFailedDialog(R.string.error_file_creation); + else + continueSelectedDownload(storageNew); + break; + } + }); + + askDialog.create().show(); } private void continueSelectedDownload(@NonNull StoredFileHelper storage) { - final Context context = getContext(); - if (!storage.canWrite()) { showFailedDialog(R.string.permission_denied); return; @@ -678,7 +721,6 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck if (storage.length() > 0) storage.truncate(); } catch (IOException e) { Log.e(TAG, "failed to overwrite the file: " + storage.getUri().toString(), e); - //showErrorActivity(e); showFailedDialog(R.string.overwrite_failed); return; } @@ -748,7 +790,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck if (secondaryStreamUrl == null) { urls = new String[]{selectedStream.getUrl()}; } else { - urls = new String[]{selectedStream.getUrl(), secondaryStreamUrl}; + urls = new String[]{secondaryStreamUrl, selectedStream.getUrl()}; } DownloadManagerService.startMission(context, urls, storage, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength); diff --git a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java index 3737d1c17..5d4ccf3f8 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java @@ -14,18 +14,23 @@ import android.support.v7.preference.Preference; import android.util.Log; import android.widget.Toast; +import com.nononsenseapps.filepicker.Utils; + import org.schabi.newpipe.R; import org.schabi.newpipe.util.FilePickerActivityHelper; import java.io.File; import java.io.IOException; -import java.net.URI; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import us.shandian.giga.io.StoredDirectoryHelper; public class DownloadSettingsFragment extends BasePreferenceFragment { private static final int REQUEST_DOWNLOAD_VIDEO_PATH = 0x1235; private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236; + public static final boolean IGNORE_RELEASE_OLD_PATH = true; private String DOWNLOAD_PATH_VIDEO_PREFERENCE; private String DOWNLOAD_PATH_AUDIO_PREFERENCE; @@ -35,41 +40,46 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { private Preference prefPathVideo; private Preference prefPathAudio; - + private Context ctx; + private boolean lastAPIJavaIO; + @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - initKeys(); - updatePreferencesSummary(); - } - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - addPreferencesFromResource(R.xml.download_settings); + DOWNLOAD_PATH_VIDEO_PREFERENCE = getString(R.string.download_path_video_key); + DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key); + DOWNLOAD_STORAGE_API = getString(R.string.downloads_storage_api); + DOWNLOAD_STORAGE_API_DEFAULT = getString(R.string.downloads_storage_api_default); prefPathVideo = findPreference(DOWNLOAD_PATH_VIDEO_PREFERENCE); prefPathAudio = findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE); - updatePathPickers(usingJavaIO()); + lastAPIJavaIO = usingJavaIO(); + + updatePreferencesSummary(); + updatePathPickers(lastAPIJavaIO); findPreference(DOWNLOAD_STORAGE_API).setOnPreferenceChangeListener((preference, value) -> { boolean javaIO = DOWNLOAD_STORAGE_API_DEFAULT.equals(value); - if (!javaIO && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (javaIO == lastAPIJavaIO) return true; + lastAPIJavaIO = javaIO; + + boolean res; + + if (javaIO && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + // forget save paths (if necessary) + res = forgetPath(DOWNLOAD_PATH_VIDEO_PREFERENCE); + res |= forgetPath(DOWNLOAD_PATH_AUDIO_PREFERENCE); + } else { + res = hasInvalidPath(DOWNLOAD_PATH_VIDEO_PREFERENCE) || hasInvalidPath(DOWNLOAD_PATH_AUDIO_PREFERENCE); + } + + if (res) { Toast.makeText(ctx, R.string.download_pick_path, Toast.LENGTH_LONG).show(); - - // forget save paths - forgetSAFTree(DOWNLOAD_PATH_VIDEO_PREFERENCE); - forgetSAFTree(DOWNLOAD_PATH_AUDIO_PREFERENCE); - - defaultPreferences.edit() - .putString(DOWNLOAD_PATH_VIDEO_PREFERENCE, "") - .putString(DOWNLOAD_PATH_AUDIO_PREFERENCE, "") - .apply(); - updatePreferencesSummary(); } @@ -78,6 +88,30 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { }); } + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + private boolean forgetPath(String prefKey) { + String path = defaultPreferences.getString(prefKey, ""); + if (path == null || path.isEmpty()) return true; + + if (path.startsWith("file://")) return false; + + // forget SAF path (file:// is compatible with the SAF wrapper) + forgetSAFTree(getContext(), prefKey); + defaultPreferences.edit().putString(prefKey, "").apply(); + + return true; + } + + private boolean hasInvalidPath(String prefKey) { + String value = defaultPreferences.getString(prefKey, null); + return value == null || value.isEmpty(); + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.download_settings); + } + @Override public void onAttach(Context context) { super.onAttach(context); @@ -91,20 +125,25 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { findPreference(DOWNLOAD_STORAGE_API).setOnPreferenceChangeListener(null); } - private void initKeys() { - DOWNLOAD_PATH_VIDEO_PREFERENCE = getString(R.string.download_path_video_key); - DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key); - DOWNLOAD_STORAGE_API = getString(R.string.downloads_storage_api); - DOWNLOAD_STORAGE_API_DEFAULT = getString(R.string.downloads_storage_api_default); + private void updatePreferencesSummary() { + showPathInSummary(DOWNLOAD_PATH_VIDEO_PREFERENCE, R.string.download_path_summary, prefPathVideo); + showPathInSummary(DOWNLOAD_PATH_AUDIO_PREFERENCE, R.string.download_path_audio_summary, prefPathAudio); } - private void updatePreferencesSummary() { - prefPathVideo.setSummary( - defaultPreferences.getString(DOWNLOAD_PATH_VIDEO_PREFERENCE, getString(R.string.download_path_summary)) - ); - prefPathAudio.setSummary( - defaultPreferences.getString(DOWNLOAD_PATH_AUDIO_PREFERENCE, getString(R.string.download_path_audio_summary)) - ); + private void showPathInSummary(String prefKey, @StringRes int defaultString, Preference target) { + String rawUri = defaultPreferences.getString(prefKey, null); + if (rawUri == null || rawUri.isEmpty()) { + target.setSummary(getString(defaultString)); + return; + } + + try { + rawUri = URLDecoder.decode(rawUri, StandardCharsets.UTF_8.name()); + } catch (UnsupportedEncodingException e) { + // nothing to do + } + + target.setSummary(rawUri); } private void updatePathPickers(boolean useJavaIO) { @@ -119,20 +158,25 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { ); } + // FIXME: after releasing the old path, all downloads created on the folder becomes inaccessible @RequiresApi(Build.VERSION_CODES.LOLLIPOP) - private void forgetSAFTree(String prefKey) { + private void forgetSAFTree(Context ctx, String prefKey) { + if (IGNORE_RELEASE_OLD_PATH) { + return; + } String oldPath = defaultPreferences.getString(prefKey, ""); - if (oldPath != null && !oldPath.isEmpty() && oldPath.charAt(0) != File.separatorChar) { + if (oldPath != null && !oldPath.isEmpty() && oldPath.charAt(0) != File.separatorChar && !oldPath.startsWith("file://")) { try { - StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(ctx, Uri.parse(oldPath), null); - if (!mainStorage.isDirect()) { - mainStorage.revokePermissions(); - Log.i(TAG, "revokePermissions() [uri=" + oldPath + "] ¡success!"); - } - } catch (IOException err) { - Log.e(TAG, "Error revoking Tree uri permissions [uri=" + oldPath + "]", err); + Uri uri = Uri.parse(oldPath); + + ctx.getContentResolver().releasePersistableUriPermission(uri, StoredDirectoryHelper.PERMISSION_FLAGS); + ctx.revokeUriPermission(uri, StoredDirectoryHelper.PERMISSION_FLAGS); + + Log.i(TAG, "Revoke old path permissions success on " + oldPath); + } catch (Exception err) { + Log.e(TAG, "Error revoking old path permissions on " + oldPath, err); } } } @@ -167,7 +211,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { if (safPick && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) .putExtra("android.content.extra.SHOW_ADVANCED", true) - .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | StoredDirectoryHelper.PERMISSION_FLAGS); + .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | StoredDirectoryHelper.PERMISSION_FLAGS); } else { i = new Intent(getActivity(), FilePickerActivityHelper.class) .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) @@ -208,27 +252,37 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { if (!usingJavaIO() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // steps: - // 1. acquire permissions on the new save path - // 2. save the new path, if step(1) was successful + // 1. revoke permissions on the old save path + // 2. acquire permissions on the new save path + // 3. save the new path, if step(2) was successful + final Context ctx = getContext(); + if (ctx == null) throw new NullPointerException("getContext()"); + + forgetSAFTree(ctx, key); try { + ctx.grantUriPermission(ctx.getPackageName(), uri, StoredDirectoryHelper.PERMISSION_FLAGS); + StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(ctx, uri, null); - mainStorage.acquirePermissions(); - Log.i(TAG, "acquirePermissions() [uri=" + uri.toString() + "] ¡success!"); + Log.i(TAG, "Acquiring tree success from " + uri.toString()); + + if (!mainStorage.canWrite()) + throw new IOException("No write permissions on " + uri.toString()); } catch (IOException err) { - Log.e(TAG, "Error acquiring permissions on " + uri.toString()); + Log.e(TAG, "Error acquiring tree from " + uri.toString(), err); showMessageDialog(R.string.general_error, R.string.no_available_dir); return; } - - defaultPreferences.edit().putString(key, uri.toString()).apply(); } else { - defaultPreferences.edit().putString(key, uri.toString()).apply(); - updatePreferencesSummary(); - - File target = new File(URI.create(uri.toString())); - if (!target.canWrite()) + File target = Utils.getFileForUri(data.getData()); + if (!target.canWrite()) { showMessageDialog(R.string.download_to_sdcard_error_title, R.string.download_to_sdcard_error_message); + return; + } + uri = Uri.fromFile(target); } + + defaultPreferences.edit().putString(key, uri.toString()).apply(); + updatePreferencesSummary(); } } diff --git a/app/src/main/java/org/schabi/newpipe/streams/DataReader.java b/app/src/main/java/org/schabi/newpipe/streams/DataReader.java index 567fa5229..0e62810c5 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/DataReader.java +++ b/app/src/main/java/org/schabi/newpipe/streams/DataReader.java @@ -16,6 +16,8 @@ public class DataReader { public final static int INTEGER_SIZE = 4; public final static int FLOAT_SIZE = 4; + private final static int BUFFER_SIZE = 128 * 1024;// 128 KiB + private long position = 0; private final SharpStream stream; @@ -229,7 +231,7 @@ public class DataReader { } } - private final byte[] readBuffer = new byte[8 * 1024]; + private final byte[] readBuffer = new byte[BUFFER_SIZE]; private int readOffset; private int readCount; diff --git a/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java b/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java index 61f793e5d..03aab447c 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/Mp4FromDashWriter.java @@ -12,7 +12,6 @@ import java.io.IOException; import java.nio.ByteBuffer; /** - * * @author kapodamy */ public class Mp4FromDashWriter { @@ -262,12 +261,12 @@ public class Mp4FromDashWriter { final int ftyp_size = make_ftyp(); // reserve moov space in the output stream - if (outStream.canSetLength()) { + /*if (outStream.canSetLength()) { long length = writeOffset + auxSize; outStream.setLength(length); outSeek(length); - } else { - // hard way + } else {*/ + if (auxSize > 0) { int length = auxSize; byte[] buffer = new byte[8 * 1024];// 8 KiB while (length > 0) { @@ -276,6 +275,7 @@ public class Mp4FromDashWriter { length -= count; } } + if (auxBuffer == null) { outSeek(ftyp_size); } diff --git a/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.java b/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.java index b874a9eca..37d94cd16 100644 --- a/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.java @@ -10,6 +10,9 @@ import java.util.regex.Pattern; public class FilenameUtils { + private static final String CHARSET_MOST_SPECIAL = "[\\n\\r|?*<\":\\\\>/']+"; + private static final String CHARSET_ONLY_LETTERS_AND_DIGITS = "[^\\w\\d]+"; + /** * #143 #44 #42 #22: make sure that the filename does not contain illegal chars. * @param context the context to retrieve strings and preferences from @@ -18,11 +21,28 @@ public class FilenameUtils { */ public static String createFilename(Context context, String title) { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - final String key = context.getString(R.string.settings_file_charset_key); - final String value = sharedPreferences.getString(key, context.getString(R.string.default_file_charset_value)); - Pattern pattern = Pattern.compile(value); + + final String charset_ld = context.getString(R.string.charset_letters_and_digits_value); + final String charset_ms = context.getString(R.string.charset_most_special_value); + final String defaultCharset = context.getString(R.string.default_file_charset_value); final String replacementChar = sharedPreferences.getString(context.getString(R.string.settings_file_replacement_character_key), "_"); + String selectedCharset = sharedPreferences.getString(context.getString(R.string.settings_file_charset_key), null); + + final String charset; + + if (selectedCharset == null || selectedCharset.isEmpty()) selectedCharset = defaultCharset; + + if (selectedCharset.equals(charset_ld)) { + charset = CHARSET_ONLY_LETTERS_AND_DIGITS; + } else if (selectedCharset.equals(charset_ms)) { + charset = CHARSET_MOST_SPECIAL; + } else { + charset = selectedCharset;// ¿is the user using a custom charset? + } + + Pattern pattern = Pattern.compile(charset); + return createFilename(title, pattern, replacementChar); } diff --git a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java index 1e05983d8..f6b6b459a 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java @@ -28,7 +28,7 @@ public class DownloadInitializer extends Thread { @Override public void run() { - if (mMission.current > 0) mMission.resetState(); + if (mMission.current > 0) mMission.resetState(false,true, DownloadMission.ERROR_NOTHING); int retryCount = 0; while (true) { diff --git a/app/src/main/java/us/shandian/giga/get/DownloadMission.java b/app/src/main/java/us/shandian/giga/get/DownloadMission.java index 9ec3418b0..838acc162 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMission.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMission.java @@ -2,7 +2,6 @@ package us.shandian.giga.get; import android.os.Handler; import android.os.Message; -import android.support.annotation.NonNull; import android.util.Log; import java.io.File; @@ -86,7 +85,7 @@ public class DownloadMission extends Mission { /** * the post-processing algorithm instance */ - public transient Postprocessing psAlgorithm; + public Postprocessing psAlgorithm; /** * The current resource to download, see {@code urls[current]} and {@code offsets[current]} @@ -483,7 +482,7 @@ public class DownloadMission extends Mission { if (init != null && Thread.currentThread() != init && init.isAlive()) { init.interrupt(); synchronized (blockState) { - resetState(); + resetState(false, true, ERROR_NOTHING); } return; } @@ -525,10 +524,18 @@ public class DownloadMission extends Mission { return res; } - void resetState() { + + /** + * Resets the mission state + * + * @param rollback {@code true} true to forget all progress, otherwise, {@code false} + * @param persistChanges {@code true} to commit changes to the metadata file, otherwise, {@code false} + */ + public void resetState(boolean rollback, boolean persistChanges, int errorCode) { done = 0; blocks = -1; - errCode = ERROR_NOTHING; + errCode = errorCode; + errObject = null; fallback = false; unknownLength = false; finishCount = 0; @@ -537,7 +544,10 @@ public class DownloadMission extends Mission { blockState.clear(); threads = new Thread[0]; - Utility.writeToFile(metadata, DownloadMission.this); + if (rollback) current = 0; + + if (persistChanges) + Utility.writeToFile(metadata, DownloadMission.this); } private void initializer() { @@ -633,33 +643,22 @@ public class DownloadMission extends Mission { threads[0].interrupt(); } - /** - * changes the StoredFileHelper for another and saves the changes to the metadata file - * - * @param newStorage the new StoredFileHelper instance to use - */ - public void changeStorage(@NonNull StoredFileHelper newStorage) { - storage = newStorage; - // commit changes on the metadata file - runAsync(-2, this::writeThisToFile); - } - /** * Indicates whatever the backed storage is invalid * * @return {@code true}, if storage is invalid and cannot be used */ public boolean hasInvalidStorage() { - return errCode == ERROR_PROGRESS_LOST || storage == null || storage.isInvalid(); + return errCode == ERROR_PROGRESS_LOST || storage == null || storage.isInvalid() || !storage.existsAsFile(); } /** * Indicates whatever is possible to start the mission * - * @return {@code true} is this mission is "sane", otherwise, {@code false} + * @return {@code true} is this mission its "healthy", otherwise, {@code false} */ - public boolean canDownload() { - return !(isPsFailed() || errCode == ERROR_POSTPROCESSING_HOLD) && !isFinished() && !hasInvalidStorage(); + public boolean isCorrupt() { + return (isPsFailed() || errCode == ERROR_POSTPROCESSING_HOLD) || isFinished() || hasInvalidStorage(); } private boolean doPostprocessing() { diff --git a/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java b/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java index ced579b20..7a68cd778 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java @@ -137,6 +137,10 @@ public class DownloadRunnable extends Thread { mMission.setThreadBytePosition(mId, total);// download paused, save progress for this block } catch (Exception e) { + if (DEBUG) { + Log.d(TAG, mId + ": position=" + blockPosition + " total=" + total + " stopped due exception", e); + } + mMission.setThreadBytePosition(mId, total); if (!mMission.running || e instanceof ClosedByInterruptException) break; @@ -146,10 +150,6 @@ public class DownloadRunnable extends Thread { break; } - if (DEBUG) { - Log.d(TAG, mId + ":position " + blockPosition + " retrying due exception", e); - } - retry = true; } } diff --git a/app/src/main/java/us/shandian/giga/get/FinishedMission.java b/app/src/main/java/us/shandian/giga/get/FinishedMission.java index 5540b44a1..2a01896fe 100644 --- a/app/src/main/java/us/shandian/giga/get/FinishedMission.java +++ b/app/src/main/java/us/shandian/giga/get/FinishedMission.java @@ -12,5 +12,7 @@ public class FinishedMission extends Mission { length = mission.length;// ¿or mission.done? timestamp = mission.timestamp; kind = mission.kind; + storage = mission.storage; + } } diff --git a/app/src/main/java/us/shandian/giga/get/Mission.java b/app/src/main/java/us/shandian/giga/get/Mission.java index ce201d960..a9ed08fc2 100644 --- a/app/src/main/java/us/shandian/giga/get/Mission.java +++ b/app/src/main/java/us/shandian/giga/get/Mission.java @@ -1,6 +1,5 @@ package us.shandian.giga.get; -import android.net.Uri; import android.support.annotation.NonNull; import java.io.Serializable; @@ -36,15 +35,6 @@ public abstract class Mission implements Serializable { */ public StoredFileHelper storage; - /** - * get the target file on the storage - * - * @return File object - */ - public Uri getDownloadedFileUri() { - return storage.getUri(); - } - /** * Delete the downloaded file * @@ -52,7 +42,7 @@ public abstract class Mission implements Serializable { */ public boolean delete() { if (storage != null) return storage.delete(); - return true; + return true; } /** @@ -65,6 +55,6 @@ public abstract class Mission implements Serializable { public String toString() { Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(timestamp); - return "[" + calendar.getTime().toString() + "] " + getDownloadedFileUri().getPath(); + return "[" + calendar.getTime().toString() + "] " + (storage.isInvalid() ? storage.getName() : storage.getUri()); } } diff --git a/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java b/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java index 6d63b9ff7..4650f75d0 100644 --- a/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java +++ b/app/src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java @@ -35,7 +35,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper { /** * The table name of download missions */ - private static final String FINISHED_MISSIONS_TABLE_NAME = "finished_missions"; + private static final String FINISHED_TABLE_NAME = "finished_missions"; /** * The key to the urls of a mission @@ -58,7 +58,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper { * The statement to create the table */ private static final String MISSIONS_CREATE_TABLE = - "CREATE TABLE " + FINISHED_MISSIONS_TABLE_NAME + " (" + + "CREATE TABLE " + FINISHED_TABLE_NAME + " (" + KEY_PATH + " TEXT NOT NULL, " + KEY_SOURCE + " TEXT NOT NULL, " + KEY_DONE + " INTEGER NOT NULL, " + @@ -111,7 +111,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper { ) ).toString()); - db.insert(FINISHED_MISSIONS_TABLE_NAME, null, values); + db.insert(FINISHED_TABLE_NAME, null, values); } db.setTransactionSuccessful(); db.endTransaction(); @@ -154,10 +154,10 @@ public class FinishedMissionStore extends SQLiteOpenHelper { mission.kind = kind.charAt(0); try { - mission.storage = new StoredFileHelper(context, Uri.parse(path), ""); + mission.storage = new StoredFileHelper(context,null, Uri.parse(path), ""); } catch (Exception e) { Log.e("FinishedMissionStore", "failed to load the storage path of: " + path, e); - mission.storage = new StoredFileHelper(path, "", ""); + mission.storage = new StoredFileHelper(null, path, "", ""); } return mission; @@ -170,7 +170,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper { public ArrayList loadFinishedMissions() { SQLiteDatabase database = getReadableDatabase(); - Cursor cursor = database.query(FINISHED_MISSIONS_TABLE_NAME, null, null, + Cursor cursor = database.query(FINISHED_TABLE_NAME, null, null, null, null, null, KEY_TIMESTAMP + " DESC"); int count = cursor.getCount(); @@ -188,33 +188,47 @@ public class FinishedMissionStore extends SQLiteOpenHelper { if (downloadMission == null) throw new NullPointerException("downloadMission is null"); SQLiteDatabase database = getWritableDatabase(); ContentValues values = getValuesOfMission(downloadMission); - database.insert(FINISHED_MISSIONS_TABLE_NAME, null, values); + database.insert(FINISHED_TABLE_NAME, null, values); } public void deleteMission(Mission mission) { if (mission == null) throw new NullPointerException("mission is null"); - String path = mission.getDownloadedFileUri().toString(); + String ts = String.valueOf(mission.timestamp); SQLiteDatabase database = getWritableDatabase(); - if (mission instanceof FinishedMission) - database.delete(FINISHED_MISSIONS_TABLE_NAME, KEY_TIMESTAMP + " = ?, " + KEY_PATH + " = ?", new String[]{path}); - else + if (mission instanceof FinishedMission) { + if (mission.storage.isInvalid()) { + database.delete(FINISHED_TABLE_NAME, KEY_TIMESTAMP + " = ?", new String[]{ts}); + } else { + database.delete(FINISHED_TABLE_NAME, KEY_TIMESTAMP + " = ? AND " + KEY_PATH + " = ?", new String[]{ + ts, mission.storage.getUri().toString() + }); + } + } else { throw new UnsupportedOperationException("DownloadMission"); + } } public void updateMission(Mission mission) { if (mission == null) throw new NullPointerException("mission is null"); SQLiteDatabase database = getWritableDatabase(); ContentValues values = getValuesOfMission(mission); - String path = mission.getDownloadedFileUri().toString(); + String ts = String.valueOf(mission.timestamp); int rowsAffected; - if (mission instanceof FinishedMission) - rowsAffected = database.update(FINISHED_MISSIONS_TABLE_NAME, values, KEY_PATH + " = ?", new String[]{path}); - else + if (mission instanceof FinishedMission) { + if (mission.storage.isInvalid()) { + rowsAffected = database.update(FINISHED_TABLE_NAME, values, KEY_TIMESTAMP + " = ?", new String[]{ts}); + } else { + rowsAffected = database.update(FINISHED_TABLE_NAME, values, KEY_PATH + " = ?", new String[]{ + mission.storage.getUri().toString() + }); + } + } else { throw new UnsupportedOperationException("DownloadMission"); + } if (rowsAffected != 1) { Log.e("FinishedMissionStore", "Expected 1 row to be affected by update but got " + rowsAffected); diff --git a/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java b/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java index 650725a76..327b9149e 100644 --- a/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java +++ b/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java @@ -12,7 +12,7 @@ public class CircularFileWriter extends SharpStream { private final static int QUEUE_BUFFER_SIZE = 8 * 1024;// 8 KiB private final static int NOTIFY_BYTES_INTERVAL = 64 * 1024;// 64 KiB - private final static int THRESHOLD_AUX_LENGTH = 3 * 1024 * 1024;// 3 MiB + private final static int THRESHOLD_AUX_LENGTH = 15 * 1024 * 1024;// 15 MiB private OffsetChecker callback; @@ -44,41 +44,84 @@ public class CircularFileWriter extends SharpStream { reportPosition = NOTIFY_BYTES_INTERVAL; } - private void flushAuxiliar() throws IOException { + private void flushAuxiliar(long amount) throws IOException { if (aux.length < 1) { return; } - boolean underflow = out.getOffset() >= out.length; - out.flush(); aux.flush(); + boolean underflow = aux.offset < aux.length || out.offset < out.length; + aux.target.seek(0); out.target.seek(out.length); - long length = aux.length; - out.length += aux.length; - + long length = amount; while (length > 0) { int read = (int) Math.min(length, Integer.MAX_VALUE); read = aux.target.read(aux.queue, 0, Math.min(read, aux.queue.length)); + if (read < 1) { + amount -= length; + break; + } + out.writeProof(aux.queue, read); length -= read; } if (underflow) { - out.offset += aux.offset; - out.target.seek(out.offset); + if (out.offset >= out.length) { + // calculate the aux underflow pointer + if (aux.offset < amount) { + out.offset += aux.offset; + aux.offset = 0; + out.target.seek(out.offset); + } else { + aux.offset -= amount; + out.offset = out.length + amount; + } + } else { + aux.offset = 0; + } } else { - out.offset = out.length; + out.offset += amount; + aux.offset -= amount; } + out.length += amount; + if (out.length > maxLengthKnown) { maxLengthKnown = out.length; } + if (amount < aux.length) { + // move the excess data to the beginning of the file + long readOffset = amount; + long writeOffset = 0; + byte[] buffer = new byte[128 * 1024]; // 128 KiB + + aux.length -= amount; + length = aux.length; + while (length > 0) { + int read = (int) Math.min(length, Integer.MAX_VALUE); + read = aux.target.read(buffer, 0, Math.min(read, buffer.length)); + + aux.target.seek(writeOffset); + aux.writeProof(buffer, read); + + writeOffset += read; + readOffset += read; + length -= read; + + aux.target.seek(readOffset); + } + + aux.target.setLength(aux.length); + return; + } + if (aux.length > THRESHOLD_AUX_LENGTH) { aux.target.setLength(THRESHOLD_AUX_LENGTH);// or setLength(0); } @@ -94,7 +137,7 @@ public class CircularFileWriter extends SharpStream { * @throws IOException if an I/O error occurs */ public long finalizeFile() throws IOException { - flushAuxiliar(); + flushAuxiliar(aux.length); out.flush(); @@ -148,7 +191,7 @@ public class CircularFileWriter extends SharpStream { if (end == -1) { available = Integer.MAX_VALUE; } else if (end < offsetOut) { - throw new IOException("The reported offset is invalid: " + String.valueOf(offsetOut)); + throw new IOException("The reported offset is invalid: " + end + "<" + offsetOut); } else { available = end - offsetOut; } @@ -167,16 +210,10 @@ public class CircularFileWriter extends SharpStream { length = aux.length + len; } - if (length > available || length < THRESHOLD_AUX_LENGTH) { - aux.write(b, off, len); - } else { - if (underflow) { - aux.write(b, off, len); - flushAuxiliar(); - } else { - flushAuxiliar(); - out.write(b, off, len);// write directly on the output - } + aux.write(b, off, len); + + if (length >= THRESHOLD_AUX_LENGTH && length <= available) { + flushAuxiliar(available); } } else { if (underflow) { @@ -234,8 +271,13 @@ public class CircularFileWriter extends SharpStream { @Override public void seek(long offset) throws IOException { long total = out.length + aux.length; + if (offset == total) { - return;// nothing to do + // do not ignore the seek offset if a underflow exists + long relativeOffset = out.getOffset() + aux.getOffset(); + if (relativeOffset == total) { + return; + } } // flush everything, avoid any underflow @@ -409,6 +451,9 @@ public class CircularFileWriter extends SharpStream { } protected void seek(long absoluteOffset) throws IOException { + if (absoluteOffset == offset) { + return;// nothing to do + } offset = absoluteOffset; target.seek(absoluteOffset); } diff --git a/app/src/main/java/us/shandian/giga/io/FileStreamSAF.java b/app/src/main/java/us/shandian/giga/io/FileStreamSAF.java index cb4786280..ec6629268 100644 --- a/app/src/main/java/us/shandian/giga/io/FileStreamSAF.java +++ b/app/src/main/java/us/shandian/giga/io/FileStreamSAF.java @@ -137,4 +137,9 @@ public class FileStreamSAF extends SharpStream { public void seek(long offset) throws IOException { channel.position(offset); } + + @Override + public long length() throws IOException { + return channel.size(); + } } diff --git a/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java b/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java index f5c2fd3f5..eb3c9b817 100644 --- a/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java +++ b/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java @@ -4,8 +4,10 @@ import android.annotation.TargetApi; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; +import android.database.Cursor; import android.net.Uri; import android.os.Build; +import android.provider.DocumentsContract; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; @@ -13,8 +15,13 @@ import android.support.v4.provider.DocumentFile; import java.io.File; import java.io.IOException; +import java.net.URI; import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collections; + +import static android.provider.DocumentsContract.Document.COLUMN_DISPLAY_NAME; +import static android.provider.DocumentsContract.Root.COLUMN_DOCUMENT_ID; + public class StoredDirectoryHelper { public final static int PERMISSION_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION; @@ -22,14 +29,27 @@ public class StoredDirectoryHelper { private File ioTree; private DocumentFile docTree; - private ContentResolver contentResolver; + private Context context; private String tag; @RequiresApi(Build.VERSION_CODES.LOLLIPOP) public StoredDirectoryHelper(@NonNull Context context, @NonNull Uri path, String tag) throws IOException { - this.contentResolver = context.getContentResolver(); this.tag = tag; + + if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(path.getScheme())) { + this.ioTree = new File(URI.create(path.toString())); + return; + } + + this.context = context; + + try { + this.context.getContentResolver().takePersistableUriPermission(path, PERMISSION_FLAGS); + } catch (Exception e) { + throw new IOException(e); + } + this.docTree = DocumentFile.fromTreeUri(context, path); if (this.docTree == null) @@ -37,23 +57,75 @@ public class StoredDirectoryHelper { } @TargetApi(Build.VERSION_CODES.KITKAT) - public StoredDirectoryHelper(@NonNull String location, String tag) { + public StoredDirectoryHelper(@NonNull URI location, String tag) { ioTree = new File(location); this.tag = tag; } - @Nullable public StoredFileHelper createFile(String filename, String mime) { + return createFile(filename, mime, false); + } + + public StoredFileHelper createUniqueFile(String name, String mime) { + ArrayList matches = new ArrayList<>(); + String[] filename = splitFilename(name); + String lcFilename = filename[0].toLowerCase(); + + if (docTree == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + for (File file : ioTree.listFiles()) + addIfStartWith(matches, lcFilename, file.getName()); + } else { + // warning: SAF file listing is very slow + Uri docTreeChildren = DocumentsContract.buildChildDocumentsUriUsingTree( + docTree.getUri(), DocumentsContract.getDocumentId(docTree.getUri()) + ); + + String[] projection = new String[]{COLUMN_DISPLAY_NAME}; + String selection = "(LOWER(" + COLUMN_DISPLAY_NAME + ") LIKE ?%"; + ContentResolver cr = context.getContentResolver(); + + try (Cursor cursor = cr.query(docTreeChildren, projection, selection, new String[]{lcFilename}, null)) { + if (cursor != null) { + while (cursor.moveToNext()) + addIfStartWith(matches, lcFilename, cursor.getString(0)); + } + } + } + + if (matches.size() < 1) { + return createFile(name, mime, true); + } else { + // check if the filename is in use + String lcName = name.toLowerCase(); + for (String testName : matches) { + if (testName.equals(lcName)) { + lcName = null; + break; + } + } + + // check if not in use + if (lcName != null) return createFile(name, mime, true); + } + + Collections.sort(matches, String::compareTo); + + for (int i = 1; i < 1000; i++) { + if (Collections.binarySearch(matches, makeFileName(lcFilename, i, filename[1])) < 0) + return createFile(makeFileName(filename[0], i, filename[1]), mime, true); + } + + return createFile(String.valueOf(System.currentTimeMillis()).concat(filename[1]), mime, false); + } + + private StoredFileHelper createFile(String filename, String mime, boolean safe) { StoredFileHelper storage; try { - if (docTree == null) { - storage = new StoredFileHelper(ioTree, filename, tag); - storage.sourceTree = Uri.fromFile(ioTree).toString(); - } else { - storage = new StoredFileHelper(docTree, contentResolver, filename, mime, tag); - storage.sourceTree = docTree.getUri().toString(); - } + if (docTree == null) + storage = new StoredFileHelper(ioTree, filename, mime); + else + storage = new StoredFileHelper(context, docTree, filename, mime, safe); } catch (IOException e) { return null; } @@ -63,67 +135,6 @@ public class StoredDirectoryHelper { return storage; } - public StoredFileHelper createUniqueFile(String filename, String mime) { - ArrayList existingNames = new ArrayList<>(50); - - String ext; - - int dotIndex = filename.lastIndexOf('.'); - if (dotIndex < 0 || (dotIndex == filename.length() - 1)) { - ext = ""; - } else { - ext = filename.substring(dotIndex); - filename = filename.substring(0, dotIndex - 1); - } - - String name; - if (docTree == null) { - for (File file : ioTree.listFiles()) { - name = file.getName().toLowerCase(); - if (name.startsWith(filename)) existingNames.add(name); - } - } else { - for (DocumentFile file : docTree.listFiles()) { - name = file.getName(); - if (name == null) continue; - name = name.toLowerCase(); - if (name.startsWith(filename)) existingNames.add(name); - } - } - - boolean free = true; - String lwFilename = filename.toLowerCase(); - for (String testName : existingNames) { - if (testName.equals(lwFilename)) { - free = false; - break; - } - } - - if (free) return createFile(filename, mime); - - String[] sortedNames = existingNames.toArray(new String[0]); - Arrays.sort(sortedNames); - - String newName; - int downloadIndex = 0; - do { - newName = filename + " (" + downloadIndex + ")" + ext; - ++downloadIndex; - if (downloadIndex == 1000) { // Probably an error on our side - newName = System.currentTimeMillis() + ext; - break; - } - } while (Arrays.binarySearch(sortedNames, newName) >= 0); - - - return createFile(newName, mime); - } - - public boolean isDirect() { - return docTree == null; - } - public Uri getUri() { return docTree == null ? Uri.fromFile(ioTree) : docTree.getUri(); } @@ -136,34 +147,18 @@ public class StoredDirectoryHelper { return tag; } - public void acquirePermissions() throws IOException { - if (docTree == null) return; - - try { - contentResolver.takePersistableUriPermission(docTree.getUri(), PERMISSION_FLAGS); - } catch (Throwable e) { - throw new IOException(e); - } - } - - public void revokePermissions() throws IOException { - if (docTree == null) return; - - try { - contentResolver.releasePersistableUriPermission(docTree.getUri(), PERMISSION_FLAGS); - } catch (Throwable e) { - throw new IOException(e); - } - } - public Uri findFile(String filename) { - if (docTree == null) - return Uri.fromFile(new File(ioTree, filename)); + if (docTree == null) { + File res = new File(ioTree, filename); + return res.exists() ? Uri.fromFile(res) : null; + } - // findFile() method is very slow - DocumentFile file = docTree.findFile(filename); + DocumentFile res = findFileSAFHelper(context, docTree, filename); + return res == null ? null : res.getUri(); + } - return file == null ? null : file.getUri(); + public boolean canWrite() { + return docTree == null ? ioTree.canWrite() : docTree.canWrite(); } @NonNull @@ -172,4 +167,76 @@ public class StoredDirectoryHelper { return docTree == null ? Uri.fromFile(ioTree).toString() : docTree.getUri().toString(); } + + //////////////////// + // Utils + /////////////////// + + private static void addIfStartWith(ArrayList list, @NonNull String base, String str) { + if (str == null || str.isEmpty()) return; + str = str.toLowerCase(); + if (str.startsWith(base)) list.add(str); + } + + private static String[] splitFilename(@NonNull String filename) { + int dotIndex = filename.lastIndexOf('.'); + + if (dotIndex < 0 || (dotIndex == filename.length() - 1)) + return new String[]{filename, ""}; + + return new String[]{filename.substring(0, dotIndex), filename.substring(dotIndex)}; + } + + private static String makeFileName(String name, int idx, String ext) { + return name.concat(" (").concat(String.valueOf(idx)).concat(")").concat(ext); + } + + /** + * Fast (but not enough) file/directory finder under the storage access framework + * + * @param context The context + * @param tree Directory where search + * @param filename Target filename + * @return A {@link android.support.v4.provider.DocumentFile} contain the reference, otherwise, null + */ + static DocumentFile findFileSAFHelper(@Nullable Context context, DocumentFile tree, String filename) { + if (context == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return tree.findFile(filename);// warning: this is very slow + } + + if (!tree.canRead()) return null;// missing read permission + + final int name = 0; + final int documentId = 1; + + // LOWER() SQL function is not supported + String selection = COLUMN_DISPLAY_NAME + " = ?"; + //String selection = COLUMN_DISPLAY_NAME + " LIKE ?%"; + + Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree( + tree.getUri(), DocumentsContract.getDocumentId(tree.getUri()) + ); + String[] projection = {COLUMN_DISPLAY_NAME, COLUMN_DOCUMENT_ID}; + ContentResolver contentResolver = context.getContentResolver(); + + filename = filename.toLowerCase(); + + try (Cursor cursor = contentResolver.query(childrenUri, projection, selection, new String[]{filename}, null)) { + if (cursor == null) return null; + + while (cursor.moveToNext()) { + if (cursor.isNull(name) || !cursor.getString(name).toLowerCase().startsWith(filename)) + continue; + + return DocumentFile.fromSingleUri( + context, DocumentsContract.buildDocumentUriUsingTree( + tree.getUri(), cursor.getString(documentId) + ) + ); + } + } + + return null; + } + } diff --git a/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java b/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java index 0db442f1c..f90a756a9 100644 --- a/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java +++ b/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java @@ -8,6 +8,7 @@ import android.net.Uri; import android.os.Build; import android.provider.DocumentsContract; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.provider.DocumentFile; @@ -25,79 +26,52 @@ public class StoredFileHelper implements Serializable { private transient DocumentFile docFile; private transient DocumentFile docTree; private transient File ioFile; - private transient ContentResolver contentResolver; + private transient Context context; protected String source; - String sourceTree; + private String sourceTree; protected String tag; private String srcName; private String srcType; - public StoredFileHelper(String filename, String mime, String tag) { + public StoredFileHelper(@Nullable Uri parent, String filename, String mime, String tag) { this.source = null;// this instance will be "invalid" see invalidate()/isInvalid() methods this.srcName = filename; this.srcType = mime == null ? DEFAULT_MIME : mime; + if (parent != null) this.sourceTree = parent.toString(); this.tag = tag; } @TargetApi(Build.VERSION_CODES.LOLLIPOP) - StoredFileHelper(DocumentFile tree, ContentResolver contentResolver, String filename, String mime, String tag) throws IOException { + StoredFileHelper(@Nullable Context context, DocumentFile tree, String filename, String mime, boolean safe) throws IOException { this.docTree = tree; - this.contentResolver = contentResolver; + this.context = context; - // this is very slow, because SAF does not allow overwrite - DocumentFile res = this.docTree.findFile(filename); + DocumentFile res; - if (res != null && res.exists() && res.isDirectory()) { - if (!res.delete()) - throw new IOException("Directory with the same name found but cannot delete"); - res = null; - } - - if (res == null) { - res = this.docTree.createFile(mime == null ? DEFAULT_MIME : mime, filename); + if (safe) { + // no conflicts (the filename is not in use) + res = this.docTree.createFile(mime, filename); if (res == null) throw new IOException("Cannot create the file"); + } else { + res = createSAF(context, mime, filename); } this.docFile = res; - this.source = res.getUri().toString(); - this.srcName = getName(); - this.srcType = getType(); + + this.source = docFile.getUri().toString(); + this.sourceTree = docTree.getUri().toString(); + + this.srcName = this.docFile.getName(); + this.srcType = this.docFile.getType(); } - @TargetApi(Build.VERSION_CODES.KITKAT) - public StoredFileHelper(Context context, @NonNull Uri path, String tag) throws IOException { - this.source = path.toString(); - this.tag = tag; - - if (path.getScheme() == null || path.getScheme().equalsIgnoreCase("file")) { - this.ioFile = new File(URI.create(this.source)); - } else { - DocumentFile file = DocumentFile.fromSingleUri(context, path); - if (file == null) - throw new UnsupportedOperationException("Cannot get the file via SAF"); - - this.contentResolver = context.getContentResolver(); - this.docFile = file; - - try { - this.contentResolver.takePersistableUriPermission(docFile.getUri(), StoredDirectoryHelper.PERMISSION_FLAGS); - } catch (Exception e) { - throw new IOException(e); - } - } - - this.srcName = getName(); - this.srcType = getType(); - } - - public StoredFileHelper(File location, String filename, String tag) throws IOException { + StoredFileHelper(File location, String filename, String mime) throws IOException { this.ioFile = new File(location, filename); - this.tag = tag; if (this.ioFile.exists()) { if (!this.ioFile.isFile() && !this.ioFile.delete()) @@ -108,22 +82,58 @@ public class StoredFileHelper implements Serializable { } this.source = Uri.fromFile(this.ioFile).toString(); + this.sourceTree = Uri.fromFile(location).toString(); + + this.srcName = ioFile.getName(); + this.srcType = mime; + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + public StoredFileHelper(Context context, @Nullable Uri parent, @NonNull Uri path, String tag) throws IOException { + this.tag = tag; + this.source = path.toString(); + + if (path.getScheme() == null || path.getScheme().equalsIgnoreCase(ContentResolver.SCHEME_FILE)) { + this.ioFile = new File(URI.create(this.source)); + } else { + DocumentFile file = DocumentFile.fromSingleUri(context, path); + + if (file == null) throw new RuntimeException("SAF not available"); + + this.context = context; + + if (file.getName() == null) { + this.source = null; + return; + } else { + this.docFile = file; + takePermissionSAF(); + } + } + + if (parent != null) { + if (!ContentResolver.SCHEME_FILE.equals(parent.getScheme())) + this.docTree = DocumentFile.fromTreeUri(context, parent); + + this.sourceTree = parent.toString(); + } + this.srcName = getName(); this.srcType = getType(); } + public static StoredFileHelper deserialize(@NonNull StoredFileHelper storage, Context context) throws IOException { + Uri treeUri = storage.sourceTree == null ? null : Uri.parse(storage.sourceTree); + if (storage.isInvalid()) - return new StoredFileHelper(storage.srcName, storage.srcType, storage.tag); + return new StoredFileHelper(treeUri, storage.srcName, storage.srcType, storage.tag); - StoredFileHelper instance = new StoredFileHelper(context, Uri.parse(storage.source), storage.tag); + StoredFileHelper instance = new StoredFileHelper(context, treeUri, Uri.parse(storage.source), storage.tag); - if (storage.sourceTree != null) { - instance.docTree = DocumentFile.fromTreeUri(context, Uri.parse(instance.sourceTree)); - - if (instance.docTree == null) - throw new IOException("Cannot deserialize the tree, ¿revoked permissions?"); - } + // under SAF, if the target document is deleted, conserve the filename and mime + if (instance.srcName == null) instance.srcName = storage.srcName; + if (instance.srcType == null) instance.srcType = storage.srcType; return instance; } @@ -143,13 +153,14 @@ public class StoredFileHelper implements Serializable { who.startActivityForResult(intent, requestCode); } + public SharpStream getStream() throws IOException { invalid(); if (docFile == null) return new FileStream(ioFile); else - return new FileStreamSAF(contentResolver, docFile.getUri()); + return new FileStreamSAF(context.getContentResolver(), docFile.getUri()); } /** @@ -173,6 +184,12 @@ public class StoredFileHelper implements Serializable { return docFile == null ? Uri.fromFile(ioFile) : docFile.getUri(); } + public Uri getParentUri() { + invalid(); + + return sourceTree == null ? null : Uri.parse(sourceTree); + } + public void truncate() throws IOException { invalid(); @@ -182,17 +199,17 @@ public class StoredFileHelper implements Serializable { } public boolean delete() { - invalid(); - + if (source == null) return true; if (docFile == null) return ioFile.delete(); + boolean res = docFile.delete(); try { int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION; - contentResolver.releasePersistableUriPermission(docFile.getUri(), flags); + context.getContentResolver().releasePersistableUriPermission(docFile.getUri(), flags); } catch (Exception ex) { - // ¿what happen? + // nothing to do } return res; @@ -209,18 +226,22 @@ public class StoredFileHelper implements Serializable { return docFile == null ? ioFile.canWrite() : docFile.canWrite(); } - public File getIOFile() { - return ioFile; - } - public String getName() { - if (source == null) return srcName; - return docFile == null ? ioFile.getName() : docFile.getName(); + if (source == null) + return srcName; + else if (docFile == null) + return ioFile.getName(); + + String name = docFile.getName(); + return name == null ? srcName : name; } public String getType() { - if (source == null) return srcType; - return docFile == null ? DEFAULT_MIME : docFile.getType();// not obligatory for Java IO + if (source == null || docFile == null) + return srcType; + + String type = docFile.getType(); + return type == null ? srcType : type; } public String getTag() { @@ -231,29 +252,41 @@ public class StoredFileHelper implements Serializable { if (source == null) return false; boolean exists = docFile == null ? ioFile.exists() : docFile.exists(); - boolean asFile = docFile == null ? ioFile.isFile() : docFile.isFile();// ¿docFile.isVirtual() means is no-physical? + boolean isFile = docFile == null ? ioFile.isFile() : docFile.isFile();// ¿docFile.isVirtual() means is no-physical? - return exists && asFile; + return exists && isFile; } public boolean create() { invalid(); + boolean result; if (docFile == null) { try { - return ioFile.createNewFile(); + result = ioFile.createNewFile(); + } catch (IOException e) { + return false; + } + } else if (docTree == null) { + result = false; + } else { + if (!docTree.canRead() || !docTree.canWrite()) return false; + try { + docFile = createSAF(context, srcType, srcName); + if (docFile == null || docFile.getName() == null) return false; + result = true; } catch (IOException e) { return false; } } - if (docTree == null || docFile.getName() == null) return false; + if (result) { + source = (docFile == null ? Uri.fromFile(ioFile) : docFile.getUri()).toString(); + srcName = getName(); + srcType = getType(); + } - DocumentFile res = docTree.createFile(docFile.getName(), docFile.getType() == null ? DEFAULT_MIME : docFile.getType()); - if (res == null) return false; - - docFile = res; - return true; + return result; } public void invalidate() { @@ -264,20 +297,25 @@ public class StoredFileHelper implements Serializable { source = null; - sourceTree = null; docTree = null; docFile = null; ioFile = null; - contentResolver = null; - } - - private void invalid() { - if (source == null) - throw new IllegalStateException("In invalid state"); + context = null; } public boolean equals(StoredFileHelper storage) { - if (this.isInvalid() != storage.isInvalid()) return false; + if (this == storage) return true; + + // note: do not compare tags, files can have the same parent folder + //if (stringMismatch(this.tag, storage.tag)) return false; + + if (stringMismatch(getLowerCase(this.sourceTree), getLowerCase(this.sourceTree))) + return false; + + if (this.isInvalid() || storage.isInvalid()) { + return this.srcName.equalsIgnoreCase(storage.srcName) && this.srcType.equalsIgnoreCase(storage.srcType); + } + if (this.isDirect() != storage.isDirect()) return false; if (this.isDirect()) @@ -298,4 +336,46 @@ public class StoredFileHelper implements Serializable { else return "sourceFile=" + source + " treeSource=" + (sourceTree == null ? "" : sourceTree) + " tag=" + tag; } + + + private void invalid() { + if (source == null) + throw new IllegalStateException("In invalid state"); + } + + private void takePermissionSAF() throws IOException { + try { + context.getContentResolver().takePersistableUriPermission(docFile.getUri(), StoredDirectoryHelper.PERMISSION_FLAGS); + } catch (Exception e) { + if (docFile.getName() == null) throw new IOException(e); + } + } + + private DocumentFile createSAF(@Nullable Context context, String mime, String filename) throws IOException { + DocumentFile res = StoredDirectoryHelper.findFileSAFHelper(context, docTree, filename); + + if (res != null && res.exists() && res.isDirectory()) { + if (!res.delete()) + throw new IOException("Directory with the same name found but cannot delete"); + res = null; + } + + if (res == null) { + res = this.docTree.createFile(srcType == null ? DEFAULT_MIME : mime, filename); + if (res == null) throw new IOException("Cannot create the file"); + } + + return res; + } + + private String getLowerCase(String str) { + return str == null ? null : str.toLowerCase(); + } + + private boolean stringMismatch(String str1, String str2) { + if (str1 == null && str2 == null) return false; + if ((str1 == null) != (str2 == null)) return true; + + return !str1.equals(str2); + } } diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Mp4FromDashMuxer.java b/app/src/main/java/us/shandian/giga/postprocessing/Mp4FromDashMuxer.java index 98ab29dbb..f12c1c2d2 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/Mp4FromDashMuxer.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/Mp4FromDashMuxer.java @@ -11,7 +11,7 @@ import java.io.IOException; class Mp4FromDashMuxer extends Postprocessing { Mp4FromDashMuxer() { - super(2 * 1024 * 1024/* 2 MiB */, true); + super(3 * 1024 * 1024/* 3 MiB */, true); } @Override diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java index 7bc32ea05..3d10628e7 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java @@ -8,6 +8,7 @@ import org.schabi.newpipe.streams.io.SharpStream; import java.io.File; import java.io.IOException; +import java.io.Serializable; import us.shandian.giga.get.DownloadMission; import us.shandian.giga.io.ChunkFileInputStream; @@ -19,7 +20,7 @@ import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING; import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD; import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION; -public abstract class Postprocessing { +public abstract class Postprocessing implements Serializable { static transient final byte OK_RESULT = ERROR_NOTHING; @@ -28,12 +29,10 @@ public abstract class Postprocessing { public transient static final String ALGORITHM_MP4_FROM_DASH_MUXER = "mp4D-mp4"; public transient static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a"; - public static Postprocessing getAlgorithm(String algorithmName, String[] args) { + public static Postprocessing getAlgorithm(@NonNull String algorithmName, String[] args, @NonNull File cacheDir) { Postprocessing instance; - if (null == algorithmName) { - throw new NullPointerException("algorithmName"); - } else switch (algorithmName) { + switch (algorithmName) { case ALGORITHM_TTML_CONVERTER: instance = new TtmlConverter(); break; @@ -47,13 +46,14 @@ public abstract class Postprocessing { instance = new M4aNoDash(); break; /*case "example-algorithm": - instance = new ExampleAlgorithm(mission);*/ + instance = new ExampleAlgorithm();*/ default: throw new RuntimeException("Unimplemented post-processing algorithm: " + algorithmName); } instance.args = args; - instance.name = algorithmName; + instance.name = algorithmName;// for debug only, maybe remove this field in the future + instance.cacheDir = cacheDir; return instance; } @@ -125,7 +125,6 @@ public abstract class Postprocessing { return -1; }; - // TODO: use Context.getCache() for this operation temp = new File(cacheDir, mission.storage.getName() + ".tmp"); out = new CircularFileWriter(mission.storage.getStream(), temp, checker); diff --git a/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java b/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java index 37295f2e3..3d5ecb3cd 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java @@ -13,7 +13,7 @@ import java.io.IOException; class WebMMuxer extends Postprocessing { WebMMuxer() { - super(2048 * 1024/* 2 MiB */, true); + super(5 * 1024 * 1024/* 5 MiB */, true); } @Override diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManager.java b/app/src/main/java/us/shandian/giga/service/DownloadManager.java index 3624fb6c2..479c4b92f 100644 --- a/app/src/main/java/us/shandian/giga/service/DownloadManager.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManager.java @@ -62,13 +62,15 @@ public class DownloadManager { * @param context Context for the data source for finished downloads * @param handler Thread required for Messaging */ - DownloadManager(@NonNull Context context, Handler handler) { + DownloadManager(@NonNull Context context, Handler handler, StoredDirectoryHelper storageVideo, StoredDirectoryHelper storageAudio) { if (DEBUG) { Log.d(TAG, "new DownloadManager instance. 0x" + Integer.toHexString(this.hashCode())); } mFinishedMissionStore = new FinishedMissionStore(context); mHandler = handler; + mMainStorageAudio = storageAudio; + mMainStorageVideo = storageVideo; mMissionsFinished = loadFinishedMissions(); mPendingMissionsDir = getPendingDir(context); @@ -129,91 +131,59 @@ public class DownloadManager { } for (File sub : subs) { - if (sub.isFile()) { - DownloadMission mis = Utility.readFromFile(sub); + if (!sub.isFile()) continue; - if (mis == null) { - //noinspection ResultOfMethodCallIgnored - sub.delete(); - } else { - if (mis.isFinished()) { - //noinspection ResultOfMethodCallIgnored - sub.delete(); - continue; - } - - boolean exists; - try { - mis.storage = StoredFileHelper.deserialize(mis.storage, ctx); - exists = !mis.storage.isInvalid() && mis.storage.existsAsFile(); - - } catch (Exception ex) { - Log.e(TAG, "Failed to load the file source of " + mis.storage.toString()); - mis.storage.invalidate(); - exists = false; - } - - if (mis.isPsRunning()) { - if (mis.psAlgorithm.worksOnSameFile) { - // Incomplete post-processing results in a corrupted download file - // because the selected algorithm works on the same file to save space. - if (exists && !mis.storage.delete()) - Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath()); - - exists = true; - } - - mis.psState = 0; - mis.errCode = DownloadMission.ERROR_POSTPROCESSING_STOPPED; - mis.errObject = null; - } else if (!exists) { - - StoredDirectoryHelper mainStorage = getMainStorage(mis.storage.getTag()); - - if (!mis.storage.isInvalid() && !mis.storage.create()) { - // using javaIO cannot recreate the file - // using SAF in older devices (no tree available) - // - // force the user to pick again the save path - mis.storage.invalidate(); - } else if (mainStorage != null) { - // if the user has changed the save path before this download, the original save path will be lost - StoredFileHelper newStorage = mainStorage.createFile(mis.storage.getName(), mis.storage.getType()); - if (newStorage == null) - mis.storage.invalidate(); - else - mis.storage = newStorage; - } - - if (mis.isInitialized()) { - // the progress is lost, reset mission state - DownloadMission m = new DownloadMission(mis.urls, mis.storage, mis.kind, mis.psAlgorithm); - m.timestamp = mis.timestamp; - m.threadCount = mis.threadCount; - m.source = mis.source; - m.nearLength = mis.nearLength; - m.enqueued = mis.enqueued; - m.errCode = DownloadMission.ERROR_PROGRESS_LOST; - mis = m; - } - } - - if (mis.psAlgorithm != null) mis.psAlgorithm.cacheDir = ctx.getCacheDir(); - - mis.running = false; - mis.recovered = exists; - mis.metadata = sub; - mis.maxRetry = mPrefMaxRetry; - mis.mHandler = mHandler; - - mMissionsPending.add(mis); - } + DownloadMission mis = Utility.readFromFile(sub); + if (mis == null || mis.isFinished()) { + //noinspection ResultOfMethodCallIgnored + sub.delete(); + continue; } + + boolean exists; + try { + mis.storage = StoredFileHelper.deserialize(mis.storage, ctx); + exists = !mis.storage.isInvalid() && mis.storage.existsAsFile(); + } catch (Exception ex) { + Log.e(TAG, "Failed to load the file source of " + mis.storage.toString(), ex); + mis.storage.invalidate(); + exists = false; + } + + if (mis.isPsRunning()) { + if (mis.psAlgorithm.worksOnSameFile) { + // Incomplete post-processing results in a corrupted download file + // because the selected algorithm works on the same file to save space. + if (exists && !mis.storage.delete()) + Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath()); + + exists = true; + } + + mis.psState = 0; + mis.errCode = DownloadMission.ERROR_POSTPROCESSING_STOPPED; + mis.errObject = null; + } else if (!exists) { + tryRecover(mis); + + // the progress is lost, reset mission state + if (mis.isInitialized()) + mis.resetState(true, true, DownloadMission.ERROR_PROGRESS_LOST); + } + + if (mis.psAlgorithm != null) + mis.psAlgorithm.cacheDir = pickAvailableCacheDir(ctx); + + mis.recovered = exists; + mis.metadata = sub; + mis.maxRetry = mPrefMaxRetry; + mis.mHandler = mHandler; + + mMissionsPending.add(mis); } - if (mMissionsPending.size() > 1) { + if (mMissionsPending.size() > 1) Collections.sort(mMissionsPending, (mission1, mission2) -> Long.compare(mission1.timestamp, mission2.timestamp)); - } } /** @@ -313,6 +283,25 @@ public class DownloadManager { } } + public void tryRecover(DownloadMission mission) { + StoredDirectoryHelper mainStorage = getMainStorage(mission.storage.getTag()); + + if (!mission.storage.isInvalid() && mission.storage.create()) return; + + // using javaIO cannot recreate the file + // using SAF in older devices (no tree available) + // + // force the user to pick again the save path + mission.storage.invalidate(); + + if (mainStorage == null) return; + + // if the user has changed the save path before this download, the original save path will be lost + StoredFileHelper newStorage = mainStorage.createFile(mission.storage.getName(), mission.storage.getType()); + + if (newStorage != null) mission.storage = newStorage; + } + /** * Get a pending mission by its path @@ -392,7 +381,7 @@ public class DownloadManager { synchronized (this) { for (DownloadMission mission : mMissionsPending) { - if (mission.running || !mission.canDownload()) continue; + if (mission.running || mission.isCorrupt()) continue; flag = true; mission.start(); @@ -482,7 +471,7 @@ public class DownloadManager { int paused = 0; synchronized (this) { for (DownloadMission mission : mMissionsPending) { - if (!mission.canDownload() || mission.isPsRunning()) continue; + if (mission.isCorrupt() || mission.isPsRunning()) continue; if (mission.running && isMetered) { paused++; @@ -542,6 +531,20 @@ public class DownloadManager { return MissionState.None; } + private static boolean isDirectoryAvailable(File directory) { + return directory != null && directory.canWrite(); + } + + static File pickAvailableCacheDir(@NonNull Context ctx) { + if (isDirectoryAvailable(ctx.getExternalCacheDir())) + return ctx.getExternalCacheDir(); + else if (isDirectoryAvailable(ctx.getCacheDir())) + return ctx.getCacheDir(); + + // this never should happen + return ctx.getDir("tmp", Context.MODE_PRIVATE); + } + @Nullable private StoredDirectoryHelper getMainStorage(@NonNull String tag) { if (tag.equals(TAG_AUDIO)) return mMainStorageAudio; @@ -656,7 +659,7 @@ public class DownloadManager { synchronized (DownloadManager.this) { for (DownloadMission mission : mMissionsPending) { - if (hidden.contains(mission) || mission.canDownload()) + if (hidden.contains(mission) || mission.isCorrupt()) continue; if (mission.running) diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java index deed9e8e3..da63cb545 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -6,6 +6,7 @@ import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.BroadcastReceiver; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -40,6 +41,7 @@ import org.schabi.newpipe.player.helper.LockManager; import java.io.File; import java.io.IOException; +import java.net.URI; import java.util.ArrayList; import us.shandian.giga.get.DownloadMission; @@ -65,14 +67,15 @@ public class DownloadManagerService extends Service { private static final int DOWNLOADS_NOTIFICATION_ID = 1001; private static final String EXTRA_URLS = "DownloadManagerService.extra.urls"; - private static final String EXTRA_PATH = "DownloadManagerService.extra.path"; private static final String EXTRA_KIND = "DownloadManagerService.extra.kind"; private static final String EXTRA_THREADS = "DownloadManagerService.extra.threads"; private static final String EXTRA_POSTPROCESSING_NAME = "DownloadManagerService.extra.postprocessingName"; private static final String EXTRA_POSTPROCESSING_ARGS = "DownloadManagerService.extra.postprocessingArgs"; private static final String EXTRA_SOURCE = "DownloadManagerService.extra.source"; private static final String EXTRA_NEAR_LENGTH = "DownloadManagerService.extra.nearLength"; - private static final String EXTRA_MAIN_STORAGE_TAG = "DownloadManagerService.extra.tag"; + private static final String EXTRA_PATH = "DownloadManagerService.extra.storagePath"; + private static final String EXTRA_PARENT_PATH = "DownloadManagerService.extra.storageParentPath"; + private static final String EXTRA_STORAGE_TAG = "DownloadManagerService.extra.storageTag"; private static final String ACTION_RESET_DOWNLOAD_FINISHED = APPLICATION_ID + ".reset_download_finished"; private static final String ACTION_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_finished"; @@ -136,7 +139,9 @@ public class DownloadManagerService extends Service { } }; - mManager = new DownloadManager(this, mHandler); + mPrefs = PreferenceManager.getDefaultSharedPreferences(this); + + mManager = new DownloadManager(this, mHandler, getVideoStorage(), getAudioStorage()); Intent openDownloadListIntent = new Intent(this, DownloadActivity.class) .setAction(Intent.ACTION_MAIN); @@ -182,7 +187,6 @@ public class DownloadManagerService extends Service { registerReceiver(mNetworkStateListener, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); } - mPrefs = PreferenceManager.getDefaultSharedPreferences(this); mPrefs.registerOnSharedPreferenceChangeListener(mPrefChangeListener); handlePreferenceChange(mPrefs, getString(R.string.downloads_cross_network)); @@ -190,8 +194,6 @@ public class DownloadManagerService extends Service { handlePreferenceChange(mPrefs, getString(R.string.downloads_queue_limit)); mLock = new LockManager(this); - - setupStorageAPI(true); } @Override @@ -347,11 +349,12 @@ public class DownloadManagerService extends Service { } else if (key.equals(getString(R.string.downloads_queue_limit))) { mManager.mPrefQueueLimit = prefs.getBoolean(key, true); } else if (key.equals(getString(R.string.downloads_storage_api))) { - setupStorageAPI(false); + mManager.mMainStorageVideo = loadMainStorage(getString(R.string.download_path_video_key), DownloadManager.TAG_VIDEO); + mManager.mMainStorageAudio = loadMainStorage(getString(R.string.download_path_audio_key), DownloadManager.TAG_AUDIO); } else if (key.equals(getString(R.string.download_path_video_key))) { - loadMainStorage(key, DownloadManager.TAG_VIDEO, false); + mManager.mMainStorageVideo = loadMainStorage(key, DownloadManager.TAG_VIDEO); } else if (key.equals(getString(R.string.download_path_audio_key))) { - loadMainStorage(key, DownloadManager.TAG_AUDIO, false); + mManager.mMainStorageAudio = loadMainStorage(key, DownloadManager.TAG_AUDIO); } } @@ -387,36 +390,46 @@ public class DownloadManagerService extends Service { Intent intent = new Intent(context, DownloadManagerService.class); intent.setAction(Intent.ACTION_RUN); intent.putExtra(EXTRA_URLS, urls); - intent.putExtra(EXTRA_PATH, storage.getUri()); intent.putExtra(EXTRA_KIND, kind); intent.putExtra(EXTRA_THREADS, threads); intent.putExtra(EXTRA_SOURCE, source); intent.putExtra(EXTRA_POSTPROCESSING_NAME, psName); intent.putExtra(EXTRA_POSTPROCESSING_ARGS, psArgs); intent.putExtra(EXTRA_NEAR_LENGTH, nearLength); - intent.putExtra(EXTRA_MAIN_STORAGE_TAG, storage.getTag()); + + intent.putExtra(EXTRA_PARENT_PATH, storage.getParentUri()); + intent.putExtra(EXTRA_PATH, storage.getUri()); + intent.putExtra(EXTRA_STORAGE_TAG, storage.getTag()); + context.startService(intent); } - public void startMission(Intent intent) { + private void startMission(Intent intent) { String[] urls = intent.getStringArrayExtra(EXTRA_URLS); Uri path = intent.getParcelableExtra(EXTRA_PATH); + Uri parentPath = intent.getParcelableExtra(EXTRA_PARENT_PATH); int threads = intent.getIntExtra(EXTRA_THREADS, 1); char kind = intent.getCharExtra(EXTRA_KIND, '?'); String psName = intent.getStringExtra(EXTRA_POSTPROCESSING_NAME); String[] psArgs = intent.getStringArrayExtra(EXTRA_POSTPROCESSING_ARGS); String source = intent.getStringExtra(EXTRA_SOURCE); long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0); - String tag = intent.getStringExtra(EXTRA_MAIN_STORAGE_TAG); + String tag = intent.getStringExtra(EXTRA_STORAGE_TAG); StoredFileHelper storage; try { - storage = new StoredFileHelper(this, path, tag); + storage = new StoredFileHelper(this, parentPath, path, tag); } catch (IOException e) { throw new RuntimeException(e);// this never should happen } - final DownloadMission mission = new DownloadMission(urls, storage, kind, Postprocessing.getAlgorithm(psName, psArgs)); + Postprocessing ps; + if (psName == null) + ps = null; + else + ps = Postprocessing.getAlgorithm(psName, psArgs, DownloadManager.pickAvailableCacheDir(this)); + + final DownloadMission mission = new DownloadMission(urls, storage, kind, ps); mission.threadCount = threads; mission.source = source; mission.nearLength = nearLength; @@ -525,60 +538,63 @@ public class DownloadManagerService extends Service { mLockAcquired = acquire; } - private void setupStorageAPI(boolean acquire) { - loadMainStorage(getString(R.string.download_path_audio_key), DownloadManager.TAG_VIDEO, acquire); - loadMainStorage(getString(R.string.download_path_video_key), DownloadManager.TAG_AUDIO, acquire); + private StoredDirectoryHelper getVideoStorage() { + return loadMainStorage(getString(R.string.download_path_video_key), DownloadManager.TAG_VIDEO); } - void loadMainStorage(String prefKey, String tag, boolean acquire) { + private StoredDirectoryHelper getAudioStorage() { + return loadMainStorage(getString(R.string.download_path_audio_key), DownloadManager.TAG_AUDIO); + } + + + private StoredDirectoryHelper loadMainStorage(String prefKey, String tag) { String path = mPrefs.getString(prefKey, null); final String JAVA_IO = getString(R.string.downloads_storage_api_default); boolean useJavaIO = JAVA_IO.equals(mPrefs.getString(getString(R.string.downloads_storage_api), JAVA_IO)); final String defaultPath; - if (tag.equals(DownloadManager.TAG_VIDEO)) - defaultPath = Environment.DIRECTORY_MOVIES; - else// if (tag.equals(DownloadManager.TAG_AUDIO)) - defaultPath = Environment.DIRECTORY_MUSIC; - - StoredDirectoryHelper mainStorage; - if (path == null || path.isEmpty()) { - mainStorage = useJavaIO ? new StoredDirectoryHelper(defaultPath, tag) : null; - } else { - - if (path.charAt(0) == File.separatorChar) { - Log.i(TAG, "Migrating old save path: " + path); - - useJavaIO = true; - path = Uri.fromFile(new File(path)).toString(); - - mPrefs.edit().putString(prefKey, path).apply(); - } - - if (useJavaIO) { - mainStorage = new StoredDirectoryHelper(path, tag); - } else { - - // tree api is not available in older versions - if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - mainStorage = null; - } else { - try { - mainStorage = new StoredDirectoryHelper(this, Uri.parse(path), tag); - if (acquire) mainStorage.acquirePermissions(); - } catch (IOException e) { - Log.e(TAG, "Failed to load the storage of " + tag + " from path: " + path, e); - mainStorage = null; - } - } - } + switch (tag) { + case DownloadManager.TAG_VIDEO: + defaultPath = Environment.DIRECTORY_MOVIES; + break; + case DownloadManager.TAG_AUDIO: + defaultPath = Environment.DIRECTORY_MUSIC; + break; + default: + return null; } - if (tag.equals(DownloadManager.TAG_VIDEO)) - mManager.mMainStorageVideo = mainStorage; - else// if (tag.equals(DownloadManager.TAG_AUDIO)) - mManager.mMainStorageAudio = mainStorage; + if (path == null || path.isEmpty()) { + return useJavaIO ? new StoredDirectoryHelper(new File(defaultPath).toURI(), tag) : null; + } + + if (path.charAt(0) == File.separatorChar) { + Log.i(TAG, "Migrating old save path: " + path); + + useJavaIO = true; + path = Uri.fromFile(new File(path)).toString(); + + mPrefs.edit().putString(prefKey, path).apply(); + } + + boolean override = path.startsWith(ContentResolver.SCHEME_FILE) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; + if (useJavaIO || override) { + return new StoredDirectoryHelper(URI.create(path), tag); + } + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return null;// SAF Directory API is not available in older versions + } + + try { + return new StoredDirectoryHelper(this, Uri.parse(path), tag); + } catch (Exception e) { + Log.e(TAG, "Failed to load the storage of " + tag + " from " + path, e); + Toast.makeText(this, R.string.no_available_dir, Toast.LENGTH_LONG).show(); + } + + return null; } //////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java index 4d80588e0..1892f4437 100644 --- a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java +++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java @@ -41,7 +41,9 @@ import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.NavigationHelper; +import java.io.File; import java.lang.ref.WeakReference; +import java.net.URI; import java.util.ArrayList; import java.util.Collections; @@ -346,7 +348,7 @@ public class MissionAdapter extends Adapter { uri = FileProvider.getUriForFile( mContext, BuildConfig.APPLICATION_ID + ".provider", - mission.storage.getIOFile() + new File(URI.create(mission.storage.getUri().toString())) ); } else { uri = mission.storage.getUri(); @@ -384,10 +386,18 @@ public class MissionAdapter extends Adapter { } private static String resolveMimeType(@NonNull Mission mission) { + String mimeType; + + if (!mission.storage.isInvalid()) { + mimeType = mission.storage.getType(); + if (mimeType != null && mimeType.length() > 0 && !mimeType.equals(StoredFileHelper.DEFAULT_MIME)) + return mimeType; + } + String ext = Utility.getFileExt(mission.storage.getName()); if (ext == null) return DEFAULT_MIME_TYPE; - String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1)); + mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1)); return mimeType == null ? DEFAULT_MIME_TYPE : mimeType; } @@ -476,6 +486,7 @@ public class MissionAdapter extends Adapter { return; case ERROR_PROGRESS_LOST: msg = R.string.error_progress_lost; + break; default: if (mission.errCode >= 100 && mission.errCode < 600) { msgEx = "HTTP " + mission.errCode; @@ -554,7 +565,12 @@ public class MissionAdapter extends Adapter { return true; case R.id.retry: if (mission.hasInvalidStorage()) { - mRecover.tryRecover(mission); + mDownloadManager.tryRecover(mission); + if (mission.storage.isInvalid()) + mRecover.tryRecover(mission); + else + recoverMission(mission); + return true; } mission.psContinue(true); @@ -672,13 +688,12 @@ public class MissionAdapter extends Adapter { if (mDeleter != null) mDeleter.resume(); } - public void recoverMission(DownloadMission mission, StoredFileHelper newStorage) { + public void recoverMission(DownloadMission mission) { for (ViewHolderItem h : mPendingDownloadsItems) { if (mission != h.item.mission) continue; - mission.changeStorage(newStorage); - mission.errCode = DownloadMission.ERROR_NOTHING; mission.errObject = null; + mission.resetState(true, false, DownloadMission.ERROR_NOTHING); h.status.setText(UNDEFINED_PROGRESS); h.state = -1; @@ -822,9 +837,9 @@ public class MissionAdapter extends Adapter { if (mission != null) { if (mission.hasInvalidStorage()) { - retry.setEnabled(true); - delete.setEnabled(true); - showError.setEnabled(true); + retry.setVisible(true); + delete.setVisible(true); + showError.setVisible(true); } else if (mission.isPsRunning()) { switch (mission.errCode) { case ERROR_INSUFFICIENT_STORAGE: diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java index 82ab777b0..bd5ce9215 100644 --- a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java +++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java @@ -9,6 +9,7 @@ import android.content.SharedPreferences; import android.os.Bundle; import android.os.IBinder; import android.preference.PreferenceManager; +import android.support.annotation.NonNull; import android.support.v4.app.Fragment; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.LinearLayoutManager; @@ -66,14 +67,7 @@ public class MissionsFragment extends Fragment { mAdapter = new MissionAdapter(mContext, mBinder.getDownloadManager(), mEmpty); mAdapter.deleterLoad(getView()); - mAdapter.setRecover(mission -> - StoredFileHelper.requestSafWithFileCreation( - MissionsFragment.this, - REQUEST_DOWNLOAD_PATH_SAF, - mission.storage.getName(), - mission.storage.getType() - ) - ); + mAdapter.setRecover(MissionsFragment.this::recoverMission); setAdapterButtons(); @@ -92,7 +86,7 @@ public class MissionsFragment extends Fragment { }; @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.missions, container, false); mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); @@ -239,8 +233,18 @@ public class MissionsFragment extends Fragment { mAdapter.setMasterButtons(mStart, mPause); } + private void recoverMission(@NonNull DownloadMission mission) { + unsafeMissionTarget = mission; + StoredFileHelper.requestSafWithFileCreation( + MissionsFragment.this, + REQUEST_DOWNLOAD_PATH_SAF, + mission.storage.getName(), + mission.storage.getType() + ); + } + @Override - public void onSaveInstanceState(Bundle outState) { + public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); if (mAdapter != null) { @@ -285,8 +289,9 @@ public class MissionsFragment extends Fragment { } try { - StoredFileHelper storage = new StoredFileHelper(mContext, data.getData(), unsafeMissionTarget.storage.getTag()); - mAdapter.recoverMission(unsafeMissionTarget, storage); + String tag = unsafeMissionTarget.storage.getTag(); + unsafeMissionTarget.storage = new StoredFileHelper(mContext, null, data.getData(), tag); + mAdapter.recoverMission(unsafeMissionTarget); } catch (IOException e) { Toast.makeText(mContext, R.string.general_error, Toast.LENGTH_LONG).show(); } diff --git a/app/src/main/java/us/shandian/giga/util/Utility.java b/app/src/main/java/us/shandian/giga/util/Utility.java index d77e598d8..793cbea18 100644 --- a/app/src/main/java/us/shandian/giga/util/Utility.java +++ b/app/src/main/java/us/shandian/giga/util/Utility.java @@ -9,6 +9,7 @@ import android.support.annotation.DrawableRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; +import android.util.Log; import android.widget.Toast; import org.schabi.newpipe.R; @@ -81,6 +82,7 @@ public class Utility { objectInputStream = new ObjectInputStream(new FileInputStream(file)); object = (T) objectInputStream.readObject(); } catch (Exception e) { + Log.e("Utility", "Failed to deserialize the object", e); object = null; } diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index eee110474..4cc394357 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -442,8 +442,8 @@ abrir en modo popup Mostrar error Codigo - No se puede crear la carpeta de destino - No se puede crear el archivo + No se puede crear el archivo + No se puede crear la carpeta de destino Permiso denegado por el sistema Fallo la conexión segura No se pudo encontrar el servidor diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index b2be3135b..42df857c1 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -176,13 +176,17 @@ - file_rename + file_rename_charset file_replacement_character _ + + CHARSET_LETTERS_AND_DIGITS + CHARSET_MOST_SPECIAL + @string/charset_letters_and_digits_value - @string/charset_most_special_characters_value + @string/charset_most_special_value @@ -190,8 +194,8 @@ @string/charset_most_special_characters - @string/charset_most_special_characters_value - + @string/charset_most_special_value + downloads_max_retry 3 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7907d2974..10b36c1c7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -305,8 +305,7 @@ Allowed characters in filenames Invalid characters are replaced with this value Replacement character - [^\\w\\d]+ - [\\n\\r|\\?*<":>/']+ + Letters and digits Most special characters No app installed to play this file diff --git a/app/src/main/res/xml/download_settings.xml b/app/src/main/res/xml/download_settings.xml index bbb91acac..2f62aa89e 100644 --- a/app/src/main/res/xml/download_settings.xml +++ b/app/src/main/res/xml/download_settings.xml @@ -5,12 +5,6 @@ android:title="@string/settings_category_downloads_title"> - - !dT z{{J+J=)G!mKm7V>FnHTRNF~i)+rCPr6nwmtY!K5qdl-)CP?!~1PF_ql$K^u3@{HWrM2FYj7Y58toT^@550Zs&_6<*JEXEecIa`pN!& z(`MT$RgL25*U!INpEsAZec`pQLl;^KBIi_0d0fO%wuv@|FoaT=p(bJ0F~lQ}q6!dsue-)woM3=P zbS?kT>w)e0&^Zq}U5?X<;0vinn)IMEeGZcPHO^Tq%fK4<|1x!!;2d+8&Z04$Di3zr zHH?}om{>DmA-M9ISHX9-ItrF|Vb8%DCsFtoChh8`;zR`iQSg`)?UubZc%*Y4t<+$i z#7VlK6E)GLa+OV%YMR@0@lW-`<4n{i`)Tx)#_0~&+adqXq9-Q#gns#lvRAze*plYl zYbajAALEGsZ`u57I63-j#M>KatcJF)0J%5e)ScemTKjUSyXWaCe?(@!T(E0jbooyT z9(!Y`@?uLgk0XIX**K>z@fcQ=#M9Qmii2sj`JoQh;qkPZEO0(ryo{o?%YF)~5W%01 zwT#zeLW?(bFb=vLpHn8D_=E))q_uM9d(Y|?sCI?4J_qTYdA$_T#R&c1^}paWINdF$ zv-d%2kh)i-rcYE-5n#Lqwfjcxt++R4BA6u07QJ2sj-dDZ(>r84=^8^fxP1|BA5L-K zzov5=%zU{boF41h?@;c1#?jem#lX#l$-uc9>k9-aJ-BwIkCBN$u&$8Z1 z(O!r7U4yuJf!{!W_k-kjr2?D>sicx2y^HiN(z{6SBE5UP>D}#kjU+FUynPDQ7b2Mo zmZpK^7fc~QhC-;`O{i`(4%IEBsPL~;qhO*&;iJ?jEF!v!cq&n(fFcDHDWFIJMG7cVK#>B96tLY8B5~+U z(3!yQ53e&>O?4)d=uWnf!y%sKn_cQmdq|nijw+Ktbgw~1-kW>x|3>Rs-0m@VvY$wLQ6t$7gMLHMhT%>c6&b{7r?n~(njv_!>w^wOh9I%1lyob6| z=2r(?d3ryvqbEk|u1D)OB&YwdML->$OI3!UtuBGwvg9VMT`l{crv)D0{%0XPseTO~^iRZ8gnyDe>fyMy8sH z&CC^Oa`5O?Xgp;mEN`m35Zm*O6HR@`aL#Jl=oH*4K21fx<=E%o)4->JPt#+cMk7Rr zTXXX|kDhmH{01_Q(Y8laR%Y!t->cnqEYr?Tb=UN|`y^G|t38L5?V)A5zIA6<#H@{c z9ecI6V8W~0LrZ-t`I*y2<@E4@>*g%t zn4X?Vg05$GSvHpg$~Ldi{OqYkjJmj2v3W%c_N%seh2>zo3W%lG=*pi(e&ep zrdZGq?X-t+IGs#bnnF1Rv3o{r!4`~4Y>*0(>83=cV`~|7y@l;6HiC~q^iGAQ9gs$R z-SbDLMl9QKGGO1dZ+g3mg9bd>((p~c%JPJBnr`g+Ay+bBzX~j6z)}V*&%`>0cGfXy zEpAxhUuq?TZZtj)+_~%Y3~<%`$g+v+-s#F&xZyLuYul+xO3f#((}prwDZyqdm#hsi zsENVy^9_AV*6;w2!}hE&_zJ>jWDWgtY`UhM~u!!<$a zo>6+dQjgy?q*gTKuiixkE WHc!-_si5<(pZ*5+VQC&K{Qv;aZ3ZF$ literal 2520 zcmV;}2`Ba+iwFP!000021MOYga;rENeLr8}@_9`Z?w3v{T{F`))elqERWoOvDH#-- zJ{UYgB*!oF?Je2l0vO)_cSx42asdl1NoYx%rM<<^zx-GxlSjt$I7{!QI?<*RmWEjr zr}Mk%f4}|n+ME7z|K;Z>ra#G_Ij75sydurj(cN?*#OkM;o2REIlB{zoGES1XAUV7F zKTQ&PBNN?B@4rkYZ!?Ifpz2(CmI}e+Ss~aYrAu} zyXn`5@-w|DGrFndv?uo!owFHd^m}{en*8|wd6};muV=hmWqB-yRP>hp zx}WUpJzz6Oyk6e1$8B?KorEHf*~~s?5}Frr#PS23xtgXWr$w3U=1{xW({*f`D9%?2 zUGJ|#JyA&tN?v;ipu&tJ|Dg5 z@Y_3AcI00r*i81%$@S!$x_J6(j!R=aj_#(vYgH=L+fQaE&4C7;H_cd%dKS?P+r4s# z>pQMhXKJ(6+F)k0Ito{HPUi{RiF9k2@*jDuo5YeQ&O=@@L8k{s=O~t3I4gISuw@Wt zJZ&20eK+9-YBR?+oAG&m^@;2FZ$C~4tObkb3(*edkSdmMo(EQYVyQf@C6c{;zFfMIt7OZ$IA0F1hcedUORvqw8!4h;X zpH1dNS80|d(c9YOLjk=U=Z6M`&1f9Fq;F0s2cNua-AfueEf3#&CcU=XB8#{WE6LJ% zZ(z-Pe;jqr!P?!UkDJC%M@x$&+02`Vpc-b^=~zV?|F2*ds8qxrXpx9OW>>I&Z`w|a z=yD9o8GnpJ7F>`>at{CSJnEryze6JRfOIsIuJzGzso=@QNc5l0U+@S#`e-wf`W`d_ zjX&o2-iK&a11sEEC2(3qaUrVq-dla_2U zzNAU;kTq8X)Vaw6UB=1!Zo16UEML)(O((fntHn)iT4wQ&tl%-@=Pwx@M$c)QzW{qL z$iK1Vk%>5@*MHBF=)3}yd^)uZ%_96hOEz8VrsHimxLT(XPpf{cj%|MgWNm@)+~vr( zt$jIE!G4H9`PCG>xmHwl`P8&mZ=2IVHp_TJgFM7Zas)`-Gxm&{99W;$h+gZ2`T?-* z$*t2v2QLV{s~3b}6W?~+E0<`tZ+QcHLDZ0LO;a*gcdvZno{#(o1e_p7>;zGEWw=7X6#}9}7b2~AHs>tQ0})?v z2?`P)*aT4mq69<sTaW zkst^IJOYn;S(;V3Uy@A{^IM;gU=$ejUL3~@PA}>;j^$Yp=|es!t&)sJjKj(Ul|ZF# zPfLqpIZJ7rAlepug69xChu}E`&mnjYjmvY$?Cd#298+`sE8QlJuDL^c4potEb)6WN z?Oo|U(a%PH7oJ0XG~qFxL!Qjv?jgeHD9<5t2+tw4*>)1=0&yL=H(0Gy;tfS_>9| zMObJ8pTH-?F@Z{;(x-@4#~aQ-COAOA0Rj#XaDadV#Hbt~?5++Fx=l>OHdO?Us}H&R zaTVy+Fo>mTrV3#)^bzlU+(-Kz_dX`g9e)7Rzv5g3rPcX|Y(%&`z~y1YE)UP1J9rfn zTp!^2(1Q{I9zlkHrq_u{%B%`vX;1vX&f=JnKHiK3IS+Cki4l`8k z1KSt?r}URM@8&yq&Qt<>z-bg0`JQT?sdlGPKsnEz+UsN(9kUJ+md^LzBD*WZ!A@`{ zLM|v5+W|3%SRS_N=-q{SRhPp!dS{%!+~hyW*0pOUpm!W-n5C(lily0*HwK^QZzcaBK5;b7Q%IUbx;5o&Y$|iWB(^zf zsi&<8+mlL5#P)pSKvLf^oRgB8#2QxYIns97cOC2+*fp?gKDb?@5u(Gi8NJ-2S4|tg zgW6;CZ56e7v-{m&$>BPdX-^-!gsfw zpS&y8nbYmx!98b`hl^kl=hEm|!?hi*?IU(=hbJ!l2jPg@g+Za5eUyn5L{waEH?m** z1cO^U*z}3_#{2|s?O@U;WRiMJ!R|>Q5;lB6ga{%;5FvsHk@0N!qHCR=Ezy(nI?lx8sZDjBSq>NZ^pbKa2#9Jt?4sLviSVNHh#t zoZ{6Ro7qJH{|`q6sa i<;6ca?q_?P&^6=tU$zJGXU^&J{>z`GBdjay_y7Pr;@O)3 From 1089de6321ccce883fbd5bf9fe8a7c1c2db1febb Mon Sep 17 00:00:00 2001 From: kapodamy Date: Sun, 14 Apr 2019 22:51:59 -0300 Subject: [PATCH 130/138] Add confirm dialog before clear the finished download list --- .../us/shandian/giga/ui/fragment/MissionsFragment.java | 8 +++++++- app/src/main/res/values-es/strings.xml | 3 ++- app/src/main/res/values/strings.xml | 1 + 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java index bd5ce9215..f8cecbed9 100644 --- a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java +++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java @@ -1,6 +1,7 @@ package us.shandian.giga.ui.fragment; import android.app.Activity; +import android.app.AlertDialog; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -179,7 +180,12 @@ public class MissionsFragment extends Fragment { updateList(); return true; case R.id.clear_list: - mAdapter.clearFinishedDownloads(); + AlertDialog.Builder prompt = new AlertDialog.Builder(mContext); + prompt.setTitle(R.string.clear_finished_download); + prompt.setMessage(R.string.confirm_prompt); + prompt.setPositiveButton(android.R.string.ok, (dialog, which) -> mAdapter.clearFinishedDownloads()); + prompt.setNegativeButton(R.string.cancel, null); + prompt.create().show(); return true; case R.id.start_downloads: item.setVisible(false); diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 4cc394357..af5b8b213 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -427,7 +427,8 @@ abrir en modo popup Mostrar como grilla Mostrar como lista Limpiar descargas finalizadas - Continúa tus %s transferencias pendientes desde Descargas + ¿Estas seguro? + Tienes %s descargas pendientes, ve a Descargas para continuarlas Detener Intentos máximos Cantidad máxima de intentos antes de cancelar la descarga diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 10b36c1c7..9df9bd051 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -536,6 +536,7 @@ Progress lost, because the file was deleted Clear finished downloads + Are you sure? Continue your %s pending transfers from Downloads Stop Maximum retries From 4b3eb2ece58cf35a930293d6a1e60961337f28d9 Mon Sep 17 00:00:00 2001 From: kapodamy Date: Mon, 15 Apr 2019 21:31:33 -0300 Subject: [PATCH 131/138] Forget the download save path if the storage API is changed --- .../settings/DownloadSettingsFragment.java | 92 ++++++++++--------- .../giga/service/DownloadManagerService.java | 5 +- 2 files changed, 51 insertions(+), 46 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java index 5d4ccf3f8..9fbf7dbea 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java @@ -2,6 +2,7 @@ package org.schabi.newpipe.settings; import android.app.Activity; import android.app.AlertDialog; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.net.Uri; @@ -22,6 +23,7 @@ import org.schabi.newpipe.util.FilePickerActivityHelper; import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.net.URI; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; @@ -30,7 +32,7 @@ import us.shandian.giga.io.StoredDirectoryHelper; public class DownloadSettingsFragment extends BasePreferenceFragment { private static final int REQUEST_DOWNLOAD_VIDEO_PATH = 0x1235; private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236; - public static final boolean IGNORE_RELEASE_OLD_PATH = true; + public static final boolean IGNORE_RELEASE_ON_OLD_PATH = true; private String DOWNLOAD_PATH_VIDEO_PREFERENCE; private String DOWNLOAD_PATH_AUDIO_PREFERENCE; @@ -68,19 +70,14 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { if (javaIO == lastAPIJavaIO) return true; lastAPIJavaIO = javaIO; - boolean res; - - if (javaIO && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - // forget save paths (if necessary) - res = forgetPath(DOWNLOAD_PATH_VIDEO_PREFERENCE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + boolean res = forgetPath(DOWNLOAD_PATH_VIDEO_PREFERENCE); res |= forgetPath(DOWNLOAD_PATH_AUDIO_PREFERENCE); - } else { - res = hasInvalidPath(DOWNLOAD_PATH_VIDEO_PREFERENCE) || hasInvalidPath(DOWNLOAD_PATH_AUDIO_PREFERENCE); - } - if (res) { - Toast.makeText(ctx, R.string.download_pick_path, Toast.LENGTH_LONG).show(); - updatePreferencesSummary(); + if (res) { + Toast.makeText(ctx, R.string.download_pick_path, Toast.LENGTH_SHORT).show(); + updatePreferencesSummary(); + } } updatePathPickers(javaIO); @@ -88,25 +85,6 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { }); } - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - private boolean forgetPath(String prefKey) { - String path = defaultPreferences.getString(prefKey, ""); - if (path == null || path.isEmpty()) return true; - - if (path.startsWith("file://")) return false; - - // forget SAF path (file:// is compatible with the SAF wrapper) - forgetSAFTree(getContext(), prefKey); - defaultPreferences.edit().putString(prefKey, "").apply(); - - return true; - } - - private boolean hasInvalidPath(String prefKey) { - String value = defaultPreferences.getString(prefKey, null); - return value == null || value.isEmpty(); - } - @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { addPreferencesFromResource(R.xml.download_settings); @@ -137,6 +115,15 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { return; } + if (rawUri.charAt(0) == File.separatorChar) { + target.setSummary(rawUri); + return; + } + if (rawUri.startsWith(ContentResolver.SCHEME_FILE)) { + target.setSummary(new File(URI.create(rawUri)).getPath()); + return; + } + try { rawUri = URLDecoder.decode(rawUri, StandardCharsets.UTF_8.name()); } catch (UnsupportedEncodingException e) { @@ -146,6 +133,24 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { target.setSummary(rawUri); } + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + private boolean forgetPath(String prefKey) { + String path = defaultPreferences.getString(prefKey, ""); + if (path == null || path.isEmpty()) return true; + + // forget SAF path if necessary + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + forgetSAFTree(getContext(), path); + + defaultPreferences.edit().putString(prefKey, "").apply(); + + return true; + } + + private boolean isFileUri(String path) { + return path.charAt(0) == File.separatorChar || path.startsWith(ContentResolver.SCHEME_FILE); + } + private void updatePathPickers(boolean useJavaIO) { boolean enabled = useJavaIO || Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; prefPathVideo.setEnabled(enabled); @@ -159,25 +164,22 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { } // FIXME: after releasing the old path, all downloads created on the folder becomes inaccessible - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) - private void forgetSAFTree(Context ctx, String prefKey) { - if (IGNORE_RELEASE_OLD_PATH) { + private void forgetSAFTree(Context ctx, String oldPath) { + if (IGNORE_RELEASE_ON_OLD_PATH) { return; } - String oldPath = defaultPreferences.getString(prefKey, ""); + if (oldPath == null || oldPath.isEmpty() || isFileUri(oldPath)) return; - if (oldPath != null && !oldPath.isEmpty() && oldPath.charAt(0) != File.separatorChar && !oldPath.startsWith("file://")) { - try { - Uri uri = Uri.parse(oldPath); + try { + Uri uri = Uri.parse(oldPath); - ctx.getContentResolver().releasePersistableUriPermission(uri, StoredDirectoryHelper.PERMISSION_FLAGS); - ctx.revokeUriPermission(uri, StoredDirectoryHelper.PERMISSION_FLAGS); + ctx.getContentResolver().releasePersistableUriPermission(uri, StoredDirectoryHelper.PERMISSION_FLAGS); + ctx.revokeUriPermission(uri, StoredDirectoryHelper.PERMISSION_FLAGS); - Log.i(TAG, "Revoke old path permissions success on " + oldPath); - } catch (Exception err) { - Log.e(TAG, "Error revoking old path permissions on " + oldPath, err); - } + Log.i(TAG, "Revoke old path permissions success on " + oldPath); + } catch (Exception err) { + Log.e(TAG, "Error revoking old path permissions on " + oldPath, err); } } @@ -258,7 +260,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { final Context ctx = getContext(); if (ctx == null) throw new NullPointerException("getContext()"); - forgetSAFTree(ctx, key); + forgetSAFTree(ctx, defaultPreferences.getString(key, "")); try { ctx.grantUriPermission(ctx.getPackageName(), uri, StoredDirectoryHelper.PERMISSION_FLAGS); diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java index da63cb545..8d838ccc2 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -566,7 +566,10 @@ public class DownloadManagerService extends Service { } if (path == null || path.isEmpty()) { - return useJavaIO ? new StoredDirectoryHelper(new File(defaultPath).toURI(), tag) : null; + if (useJavaIO) + return new StoredDirectoryHelper(new File(defaultPath).toURI(), tag); + else + return null; } if (path.charAt(0) == File.separatorChar) { From 16d6bda85d7c487086c574e44250badd315a6cdf Mon Sep 17 00:00:00 2001 From: kapodamy Date: Tue, 16 Apr 2019 23:28:03 -0300 Subject: [PATCH 132/138] Webm muxer fixes and strings.xml changes * replace "In queue" to "Pending" in the downloads header to avoid confusions (all languages) * use 29bits Clusters size to support huge video resolutions (fixes #2291) (WebmWriter.java) * add missing changes to WebmMuxer.java (i forget select the audio track) --- .../org/schabi/newpipe/streams/WebMWriter.java | 7 +++---- .../giga/postprocessing/WebMMuxer.java | 18 +++++++++++------- app/src/main/res/values-ar/strings.xml | 2 +- app/src/main/res/values-ca/strings.xml | 2 +- app/src/main/res/values-cmn/strings.xml | 2 +- app/src/main/res/values-da/strings.xml | 1 + app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-es/strings.xml | 1 + app/src/main/res/values-eu/strings.xml | 2 +- app/src/main/res/values-he/strings.xml | 2 +- app/src/main/res/values-id/strings.xml | 2 +- app/src/main/res/values-it/strings.xml | 2 +- app/src/main/res/values-ja/strings.xml | 6 +++--- app/src/main/res/values-ms/strings.xml | 2 +- app/src/main/res/values-nb-rNO/strings.xml | 1 - app/src/main/res/values-nl-rBE/strings.xml | 1 - app/src/main/res/values-nl/strings.xml | 2 +- app/src/main/res/values-pl/strings.xml | 2 +- app/src/main/res/values-pt-rBR/strings.xml | 2 +- app/src/main/res/values-pt/strings.xml | 2 +- app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values-sq/strings.xml | 1 + app/src/main/res/values-tr/strings.xml | 2 +- app/src/main/res/values-vi/strings.xml | 2 +- app/src/main/res/values-zh-rTW/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 26 files changed, 38 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java b/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java index 26b9cbebf..98261b0c9 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/WebMWriter.java @@ -321,9 +321,8 @@ public class WebMWriter { for (int i = 0; i < clusterSizes.size(); i++) { seekTo(out, clusterOffsets.get(i)); - byte[] size = ByteBuffer.allocate(4).putInt(clusterSizes.get(i) | 0x200000).array(); - out.write(size, 1, 3); - written += 3; + byte[] buffer = ByteBuffer.allocate(4).putInt(clusterSizes.get(i) | 0x10000000).array(); + dump(buffer, out); } } @@ -451,7 +450,7 @@ public class WebMWriter { /* cluster */ dump(new byte[]{0x1f, 0x43, (byte) 0xb6, 0x75}, stream); clusterOffsets.add(written);// warning: max cluster size is 256 MiB - dump(new byte[]{0x20, 0x00, 0x00}, stream); + dump(new byte[]{0x10, 0x00, 0x00, 0x00}, stream); startOffset = written;// size for the this cluster diff --git a/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java b/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java index 3d5ecb3cd..618c1ec5a 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java @@ -22,16 +22,20 @@ class WebMMuxer extends Postprocessing { muxer.parseSources(); // youtube uses a webm with a fake video track that acts as a "cover image" - WebMTrack[] tracks = muxer.getTracksFromSource(1); - int audioTrackIndex = 0; - for (int i = 0; i < tracks.length; i++) { - if (tracks[i].kind == TrackKind.Audio) { - audioTrackIndex = i; - break; + int[] indexes = new int[sources.length]; + + for (int i = 0; i < sources.length; i++) { + WebMTrack[] tracks = muxer.getTracksFromSource(i); + for (int j = 0; j < tracks.length; j++) { + if (tracks[j].kind == TrackKind.Audio) { + indexes[i] = j; + i = sources.length; + break; + } } } - muxer.selectTracks(0, audioTrackIndex); + muxer.selectTracks(indexes); muxer.build(out); return OK_RESULT; diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index dbf015c87..cc36e40bf 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -451,7 +451,7 @@ يتوفر تحديث ل newpipe! اضغط لتنزيل انتهى - في قائمة الانتظار + ريثما متوقف في قائمة الانتظار قيد المعالجة diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index f606281f4..04bb36ea3 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -425,7 +425,7 @@ Automàtic Canvia la vista Està disponible una nova actualització del NewPipe! - A la cua + Pendent en pausa a la cua Afegeix a la cua diff --git a/app/src/main/res/values-cmn/strings.xml b/app/src/main/res/values-cmn/strings.xml index 73eb43c36..7be9efc04 100644 --- a/app/src/main/res/values-cmn/strings.xml +++ b/app/src/main/res/values-cmn/strings.xml @@ -426,7 +426,7 @@ 自动 轻按以下载 已完成 - 于队列中 + 有待 已暂停 已加入队列 后处理 diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 92535310e..0c699cf0e 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -453,4 +453,5 @@ Maksimalt antal forsøg før downloaden opgives Sæt på pause ved skift til mobildata Downloads som ikke kan sættes på pause vil blive genstartet + Afventning \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index ef2789846..ae4fda922 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -437,7 +437,7 @@ NewPipe-Aktualisierung verfügbar! Zum Herunterladen antippen Fertig - In der Warteschlange + Ausstehend pausiert eingereiht Nachbearbeitung diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index af5b8b213..6a493892d 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -469,6 +469,7 @@ abrir en modo popup No es posible descargar a una tarjeta SD externa. \¿Restablecer la ubicación de la carpeta de descarga\? Seleccione los directorios de descarga + Pendiente Desuscribirse Nueva pestaña diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 2868528e9..fb41bf8ae 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -435,7 +435,7 @@ NewPipe eguneraketa eskuragarri! Sakatu deskargatzeko Amaituta - Ilaran + Zain pausatuta ilaran post-prozesua diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 41569ff0c..99f02dde8 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -440,7 +440,6 @@ יצא עדכון ל־NewPipe! יש לגעת כדי להוריד הסתיים - בתור מושהה בתור עיבוד מאוחר @@ -473,4 +472,5 @@ מספר הניסיונות החוזרים המרבי בטרם ביטול ההורדה להשהות בעת מעבר לתקשורת נתונים סלולרית הורדות שלא ניתן להשהות יופעלו מחדש + בהמתנה \ No newline at end of file diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index 31801434b..c1eb3870d 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -436,7 +436,7 @@ Pembaruan NewPipe Tersedia! Ketuk untuk mengunduh Selesai - Di antrian + Tertunda dijeda antri pengolahan-pasca diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 4ff8de734..f6d6e42f7 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -438,7 +438,7 @@ Aggiornamento di NewPipe disponibile! Premi per scaricare Finito - In coda + In attesa di in pausa in coda post-processo diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 78a20b1ab..76ccfd2dd 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -421,7 +421,7 @@ NewPipeのアップデートがあります! タップでダウンロード 終了しました - 順番に処理中 + 保留中 一時停止 順番待ちに追加しました 保存処理をしています @@ -462,6 +462,6 @@ メインページに表示されるタブ 新しいバージョンが利用可能なときにアプリの更新を確認する通知を表示します ダウンロードから %s の保留中の転送を続行します - モバイルデータ通信に切替時に、一時停止する - 一時停止できない場合は再開して継続されます + モバイルデータ通信に切り替え時に休止 + 休止できないダウンロードが再開されます \ No newline at end of file diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index ced8235f7..1f0dc3968 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -432,7 +432,6 @@ Kemas kini NewPipe Tersedia! Ketik untuk muat turun Selesai - Dalam barisan dijeda telah beratur pemprosesan-pasca @@ -465,4 +464,5 @@ Jumlah percubaan maksimum sebelum membatalkan muat turun Jeda semasa beralih ke data mudah alih Muat turun yang tidak dapat dihentikan akan dimulakan semula + Menunggu \ No newline at end of file diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 2f5d19c67..1c81feae5 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -515,7 +515,6 @@ Ny NewPipe-versjon tilgjengelig. Trykk for å laste ned Fullført - I kø pauset i kø etterbehandling diff --git a/app/src/main/res/values-nl-rBE/strings.xml b/app/src/main/res/values-nl-rBE/strings.xml index 44b2ef6ab..eac4114ff 100644 --- a/app/src/main/res/values-nl-rBE/strings.xml +++ b/app/src/main/res/values-nl-rBE/strings.xml @@ -434,7 +434,6 @@ NewPipe-update beschikbaar! Tikt voor te downloaden Voltooid - In wachtrij gepauzeerd toegevoegd aan wachtrij nabewerking diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 96de68b57..4e88b6b48 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -438,7 +438,7 @@ NewPipe-update beschikbaar! Tik om te downloaden Voltooid - In de wachtrij + In afwachting van gepauzeerd aan de wachtrij toegevoegd nabewerking diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 29070990f..d4a56256e 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -435,7 +435,6 @@ Dostępna jest aktualizacja NewPipe! Stuknij, aby pobrać Gotowe - W kolejce wstrzymane w kolejce przetwarzanie końcowe @@ -470,4 +469,5 @@ Pobierane pliki, których nie można wstrzymać, zostaną zrestartowane Zdarzenia Konferencje + Oczekuje \ No newline at end of file diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 8a16b752d..097ad1288 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -435,7 +435,6 @@ abrir em modo popup Atualização do NewPipe Disponivel! Toque para baixar Finalizado - Na fila pausado adicionado na fila pós processamento @@ -468,4 +467,5 @@ abrir em modo popup Número máximo de tentativas antes de cancelar o download Pausar quando trocar para dados móveis Downloads que não puderem ser pausados serão reiniciados + Pendente \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index a86c5b809..ddc9d503c 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -431,7 +431,6 @@ Atualização do NewPipe disponível! Toque para descarregar Terminada - Na fila em pausa na fila pós-processamento @@ -466,4 +465,5 @@ Descarregamentos que não podem ser pausados serão reiniciados Eventos Conferências + Pendente \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 620ca5619..374b9921f 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -472,4 +472,5 @@ Пост-обработка не удалась Останавливать скачивание при переходе на мобильную сеть Закрыть + в ожидании \ No newline at end of file diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml index ec31c4a97..74bf10804 100644 --- a/app/src/main/res/values-sq/strings.xml +++ b/app/src/main/res/values-sq/strings.xml @@ -48,4 +48,5 @@ Po Më vonë Standard + në pritje të diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index e518a1c0f..dd7974af2 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -438,7 +438,6 @@ NewPipe Güncellemesi Var! İndirmek için dokunun Tamamlandı - Sırada durdurulmuş sırada son işlemler uygulanıyor @@ -473,4 +472,5 @@ Duraklatılamayan indirmeler yeniden başlatılacak Olaylar Konferanslar + Kadar \ No newline at end of file diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index ff247c579..4c9f9c7d0 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -429,7 +429,6 @@ Đã có bản cập nhật NewPipe! Nhấn để tải về Xong - Trong hàng chờ đã tạm dừng trong hàng đợi đang xử lý @@ -461,6 +460,7 @@ Số lượt thử lại trước khi hủy tải về Tạm dừng tải khi chuyển qua dữ liệu di động Các tải về không thể tạm dừng được sẽ bắt đầu lại từ đầu + Đang chờ xử lý Hội thảo \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 0194418cf..023cd00c8 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -434,7 +434,7 @@ 有可用的 NewPipe 更新! 輕觸以下載 結束 - 在佇列中 + 有待 已暫停 已排入佇列 正在後處理 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9df9bd051..8433f909d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -498,7 +498,7 @@ NewPipe Update Available! Tap to download Finished - In queue + Pending paused queued post-processing From d1573a0a6e9e409df3bb8c780a4bf2222dca6272 Mon Sep 17 00:00:00 2001 From: kapodamy Date: Wed, 17 Apr 2019 18:17:24 -0300 Subject: [PATCH 133/138] misc changes * implement socket timeout error * use 128k buffer size for copy * use NewPipe HTTP user agent in the downloads * automatically recover downloads with network errors that are queued --- .../giga/get/DownloadInitializer.java | 2 +- .../us/shandian/giga/get/DownloadMission.java | 25 ++++++++++++++++--- .../shandian/giga/get/DownloadRunnable.java | 1 - .../giga/get/DownloadRunnableFallback.java | 11 ++++---- .../shandian/giga/io/CircularFileWriter.java | 7 +++--- .../giga/ui/adapter/MissionAdapter.java | 4 +++ app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values-es/strings.xml | 1 + app/src/main/res/values-eu/strings.xml | 1 + app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values-it/strings.xml | 1 + app/src/main/res/values-ja/strings.xml | 2 ++ app/src/main/res/values-nl/strings.xml | 1 + app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values-zh-rTW/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 16 files changed, 46 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java index f6b6b459a..590c3704c 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java @@ -28,7 +28,7 @@ public class DownloadInitializer extends Thread { @Override public void run() { - if (mMission.current > 0) mMission.resetState(false,true, DownloadMission.ERROR_NOTHING); + if (mMission.current > 0) mMission.resetState(false, true, DownloadMission.ERROR_NOTHING); int retryCount = 0; while (true) { diff --git a/app/src/main/java/us/shandian/giga/get/DownloadMission.java b/app/src/main/java/us/shandian/giga/get/DownloadMission.java index 838acc162..9bc46e3af 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMission.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMission.java @@ -4,11 +4,14 @@ import android.os.Handler; import android.os.Message; import android.util.Log; +import org.schabi.newpipe.Downloader; + import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.net.ConnectException; import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; import java.net.URL; import java.net.UnknownHostException; import java.util.ArrayList; @@ -45,6 +48,7 @@ public class DownloadMission extends Mission { public static final int ERROR_POSTPROCESSING_HOLD = 1009; public static final int ERROR_INSUFFICIENT_STORAGE = 1010; public static final int ERROR_PROGRESS_LOST = 1011; + public static final int ERROR_TIMEOUT = 1012; public static final int ERROR_HTTP_NO_CONTENT = 204; public static final int ERROR_HTTP_UNSUPPORTED_RANGE = 206; @@ -232,10 +236,9 @@ public class DownloadMission extends Mission { URL url = new URL(urls[current]); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setInstanceFollowRedirects(true); + conn.setRequestProperty("User-Agent", Downloader.USER_AGENT); // BUG workaround: switching between networks can freeze the download forever - - //conn.setRequestProperty("Connection", "close"); conn.setConnectTimeout(30000); conn.setReadTimeout(10000); @@ -329,6 +332,8 @@ public class DownloadMission extends Mission { notifyError(ERROR_CONNECT_HOST, null); } else if (err instanceof UnknownHostException) { notifyError(ERROR_UNKNOWN_HOST, null); + } else if (err instanceof SocketTimeoutException) { + notifyError(ERROR_TIMEOUT, null); } else { notifyError(ERROR_UNKNOWN_EXCEPTION, err); } @@ -338,7 +343,7 @@ public class DownloadMission extends Mission { Log.e(TAG, "notifyError() code = " + code, err); if (err instanceof IOException) { - if (storage.canWrite() || err.getMessage().contains("Permission denied")) { + if (!storage.canWrite() || err.getMessage().contains("Permission denied")) { code = ERROR_PERMISSION_DENIED; err = null; } else if (err.getMessage().contains("ENOSPC")) { @@ -349,7 +354,19 @@ public class DownloadMission extends Mission { errCode = code; errObject = err; - enqueued = false; + + switch (code) { + case ERROR_SSL_EXCEPTION: + case ERROR_UNKNOWN_HOST: + case ERROR_CONNECT_HOST: + case ERROR_TIMEOUT: + // do not change the queue flag for network errors, can be + // recovered silently without the user interaction + break; + default: + // also checks for server errors + if (code < 500 || code > 599) enqueued = false; + } pause(); diff --git a/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java b/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java index 7a68cd778..4380c0c68 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnable.java @@ -27,7 +27,6 @@ public class DownloadRunnable extends Thread { if (mission == null) throw new NullPointerException("mission is null"); mMission = mission; mId = id; - mConn = null; } @Override diff --git a/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java b/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java index 1a4b5d5b6..d7ff208ce 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java @@ -1,6 +1,5 @@ package us.shandian.giga.get; -import android.annotation.SuppressLint; import android.support.annotation.NonNull; import android.util.Log; @@ -19,7 +18,7 @@ import static org.schabi.newpipe.BuildConfig.DEBUG; * Single-threaded fallback mode */ public class DownloadRunnableFallback extends Thread { - private static final String TAG = "DownloadRunnableFallback"; + private static final String TAG = "DownloadRunnableFallbac"; private final DownloadMission mMission; @@ -30,9 +29,6 @@ public class DownloadRunnableFallback extends Thread { DownloadRunnableFallback(@NonNull DownloadMission mission) { mMission = mission; - mIs = null; - mF = null; - mConn = null; } private void dispose() { @@ -46,7 +42,6 @@ public class DownloadRunnableFallback extends Thread { } @Override - @SuppressLint("LongLogTag") public void run() { boolean done; @@ -106,6 +101,10 @@ public class DownloadRunnableFallback extends Thread { return; } + if (DEBUG) { + Log.e(TAG, "got exception, retrying...", e); + } + run();// try again return; } diff --git a/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java b/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java index 327b9149e..f9ceca6ad 100644 --- a/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java +++ b/app/src/main/java/us/shandian/giga/io/CircularFileWriter.java @@ -11,6 +11,7 @@ import java.io.IOException; public class CircularFileWriter extends SharpStream { private final static int QUEUE_BUFFER_SIZE = 8 * 1024;// 8 KiB + private final static int COPY_BUFFER_SIZE = 128 * 1024; // 128 KiB private final static int NOTIFY_BYTES_INTERVAL = 64 * 1024;// 64 KiB private final static int THRESHOLD_AUX_LENGTH = 15 * 1024 * 1024;// 15 MiB @@ -53,6 +54,7 @@ public class CircularFileWriter extends SharpStream { aux.flush(); boolean underflow = aux.offset < aux.length || out.offset < out.length; + byte[] buffer = new byte[COPY_BUFFER_SIZE]; aux.target.seek(0); out.target.seek(out.length); @@ -60,14 +62,14 @@ public class CircularFileWriter extends SharpStream { long length = amount; while (length > 0) { int read = (int) Math.min(length, Integer.MAX_VALUE); - read = aux.target.read(aux.queue, 0, Math.min(read, aux.queue.length)); + read = aux.target.read(buffer, 0, Math.min(read, buffer.length)); if (read < 1) { amount -= length; break; } - out.writeProof(aux.queue, read); + out.writeProof(buffer, read); length -= read; } @@ -100,7 +102,6 @@ public class CircularFileWriter extends SharpStream { // move the excess data to the beginning of the file long readOffset = amount; long writeOffset = 0; - byte[] buffer = new byte[128 * 1024]; // 128 KiB aux.length -= amount; length = aux.length; diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java index 1892f4437..21cae1835 100644 --- a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java +++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java @@ -73,6 +73,7 @@ import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD; import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_STOPPED; import static us.shandian.giga.get.DownloadMission.ERROR_PROGRESS_LOST; import static us.shandian.giga.get.DownloadMission.ERROR_SSL_EXCEPTION; +import static us.shandian.giga.get.DownloadMission.ERROR_TIMEOUT; import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION; import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_HOST; @@ -487,6 +488,9 @@ public class MissionAdapter extends Adapter { case ERROR_PROGRESS_LOST: msg = R.string.error_progress_lost; break; + case ERROR_TIMEOUT: + msg = R.string.error_timeout; + break; default: if (mission.errCode >= 100 && mission.errCode < 600) { msgEx = "HTTP " + mission.errCode; diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index ae4fda922..9a2518652 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -472,4 +472,5 @@ Downloads, die nicht pausiert werden können, werden wiederholt Konferenzen Ereignisse + Verbindungszeitüberschreitung \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 6a493892d..2e8c62bce 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -457,6 +457,7 @@ abrir en modo popup NewPipe se cerro mientras se trabajaba en el archivo No hay suficiente espacio disponible en el dispositivo Se perdió el progreso porque el archivo fue eliminado + Tiempo de espera excedido API de almacenamiento Seleccione que API utilizar para almacenar las descargas diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index fb41bf8ae..11fed8e64 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -468,4 +468,5 @@ Deskarga ezeztatu aurretik saiatu beharreko aldi kopurua Pausatu datu mugikorretara aldatzean Pausatu ezin daitezkeen deskargak berrekingo dira + Konexioaren denbora muga \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index b81195d41..ca9682fdb 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -440,6 +440,7 @@ Dans la file d\'attente En pause Téléchargement échoué + Délai de connection dépassé Conférences Téléchargement terminé %s téléchargements terminés diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index f6d6e42f7..d61b26cfb 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -473,4 +473,5 @@ I download che non possono essere messi in pausa verranno riavviati Eventi Conferenze + Connesione finita \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 76ccfd2dd..be243bffd 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -464,4 +464,6 @@ ダウンロードから %s の保留中の転送を続行します モバイルデータ通信に切り替え時に休止 休止できないダウンロードが再開されます + 保留中 + 接続タイムアウト \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 4e88b6b48..915111043 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -473,4 +473,5 @@ Downloads die niet kunnen worden gepauzeerd zullen worden herstart Gebeurtenissen Conferenties + Time-out van verbinding \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 374b9921f..c75af14e2 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -473,4 +473,5 @@ Останавливать скачивание при переходе на мобильную сеть Закрыть в ожидании + Время соединения вышло \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 023cd00c8..9b58bb79f 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -469,4 +469,5 @@ 無法暫停的下載將會重新開始 事件 會議 + 連接超時 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8433f909d..f10a9f589 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -534,6 +534,7 @@ NewPipe was closed while working on the file No space left on device Progress lost, because the file was deleted + Connection timeout Clear finished downloads Are you sure? From 34b2b9615832a9e1846b6dcfd2b8b6a0e656f162 Mon Sep 17 00:00:00 2001 From: kapodamy Date: Fri, 19 Apr 2019 16:18:19 -0300 Subject: [PATCH 134/138] Simplify the storage APIs use * use Java I/O (classic way) on older android versions * use Storage Access Framework on newer android versions (Android Lollipop or later) * both changes have the external SD Card write permission * add option to ask the save path on each download * warn the user if the save paths are not defined, this only happens on the first NewPipe run (Android Lollipop or later) --- .../newpipe/download/DownloadDialog.java | 5 +- .../settings/DownloadSettingsFragment.java | 117 +++++++----------- .../newpipe/settings/NewPipeSettings.java | 19 +-- .../giga/io/StoredDirectoryHelper.java | 5 +- .../giga/service/DownloadManagerService.java | 104 ++++++++-------- app/src/main/res/values-es/strings.xml | 12 +- app/src/main/res/values/settings_keys.xml | 15 +-- app/src/main/res/values/strings.xml | 12 +- app/src/main/res/xml/download_settings.xml | 12 +- 9 files changed, 119 insertions(+), 182 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index 8f4b569cd..f27e7467e 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -212,6 +212,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck mainStorageAudio = mgr.getMainStorageAudio(); mainStorageVideo = mgr.getMainStorageVideo(); downloadManager = mgr.getDownloadManager(); + askForSavePath = mgr.askForSavePath(); okButton.setEnabled(true); @@ -509,6 +510,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck DownloadManager downloadManager = null; ActionMenuItemView okButton = null; Context context; + boolean askForSavePath; private String getNameEditText() { String str = nameEditText.getText().toString().trim(); @@ -567,10 +569,11 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck throw new RuntimeException("No stream selected"); } - if (mainStorage == null) { + if (mainStorage == null || askForSavePath) { // This part is called if with SAF preferred: // * older android version running // * save path not defined (via download settings) + // * the user as checked the "ask where to download" option StoredFileHelper.requestSafWithFileCreation(this, REQUEST_DOWNLOAD_PATH_SAF, filename, mime); return; diff --git a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java index 9fbf7dbea..e671e4d3a 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java @@ -9,7 +9,6 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.support.annotation.Nullable; -import android.support.annotation.RequiresApi; import android.support.annotation.StringRes; import android.support.v7.preference.Preference; import android.util.Log; @@ -37,50 +36,40 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { private String DOWNLOAD_PATH_VIDEO_PREFERENCE; private String DOWNLOAD_PATH_AUDIO_PREFERENCE; - private String DOWNLOAD_STORAGE_API; - private String DOWNLOAD_STORAGE_API_DEFAULT; + private String DOWNLOAD_STORAGE_ASK; private Preference prefPathVideo; private Preference prefPathAudio; + private Preference prefStorageAsk; private Context ctx; - private boolean lastAPIJavaIO; - @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); DOWNLOAD_PATH_VIDEO_PREFERENCE = getString(R.string.download_path_video_key); DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key); - DOWNLOAD_STORAGE_API = getString(R.string.downloads_storage_api); - DOWNLOAD_STORAGE_API_DEFAULT = getString(R.string.downloads_storage_api_default); + DOWNLOAD_STORAGE_ASK = getString(R.string.downloads_storage_ask); prefPathVideo = findPreference(DOWNLOAD_PATH_VIDEO_PREFERENCE); prefPathAudio = findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE); - - lastAPIJavaIO = usingJavaIO(); + prefStorageAsk = findPreference(DOWNLOAD_STORAGE_ASK); updatePreferencesSummary(); - updatePathPickers(lastAPIJavaIO); + updatePathPickers(!defaultPreferences.getBoolean(DOWNLOAD_STORAGE_ASK, false)); - findPreference(DOWNLOAD_STORAGE_API).setOnPreferenceChangeListener((preference, value) -> { - boolean javaIO = DOWNLOAD_STORAGE_API_DEFAULT.equals(value); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + prefStorageAsk.setSummary(R.string.downloads_storage_ask_summary); + } - if (javaIO == lastAPIJavaIO) return true; - lastAPIJavaIO = javaIO; + if (hasInvalidPath(DOWNLOAD_PATH_VIDEO_PREFERENCE) || hasInvalidPath(DOWNLOAD_PATH_AUDIO_PREFERENCE)) { + Toast.makeText(ctx, R.string.download_pick_path, Toast.LENGTH_SHORT).show(); + updatePreferencesSummary(); + } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - boolean res = forgetPath(DOWNLOAD_PATH_VIDEO_PREFERENCE); - res |= forgetPath(DOWNLOAD_PATH_AUDIO_PREFERENCE); - - if (res) { - Toast.makeText(ctx, R.string.download_pick_path, Toast.LENGTH_SHORT).show(); - updatePreferencesSummary(); - } - } - - updatePathPickers(javaIO); + prefStorageAsk.setOnPreferenceChangeListener((preference, value) -> { + updatePathPickers(!(boolean) value); return true; }); } @@ -100,7 +89,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { public void onDetach() { super.onDetach(); ctx = null; - findPreference(DOWNLOAD_STORAGE_API).setOnPreferenceChangeListener(null); + prefStorageAsk.setOnPreferenceChangeListener(null); } private void updatePreferencesSummary() { @@ -133,34 +122,18 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { target.setSummary(rawUri); } - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) - private boolean forgetPath(String prefKey) { - String path = defaultPreferences.getString(prefKey, ""); - if (path == null || path.isEmpty()) return true; - - // forget SAF path if necessary - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) - forgetSAFTree(getContext(), path); - - defaultPreferences.edit().putString(prefKey, "").apply(); - - return true; - } - private boolean isFileUri(String path) { return path.charAt(0) == File.separatorChar || path.startsWith(ContentResolver.SCHEME_FILE); } - private void updatePathPickers(boolean useJavaIO) { - boolean enabled = useJavaIO || Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; - prefPathVideo.setEnabled(enabled); - prefPathAudio.setEnabled(enabled); + private boolean hasInvalidPath(String prefKey) { + String value = defaultPreferences.getString(prefKey, null); + return value == null || value.isEmpty(); } - private boolean usingJavaIO() { - return DOWNLOAD_STORAGE_API_DEFAULT.equals( - defaultPreferences.getString(DOWNLOAD_STORAGE_API, DOWNLOAD_STORAGE_API_DEFAULT) - ); + private void updatePathPickers(boolean enabled) { + prefPathVideo.setEnabled(enabled); + prefPathAudio.setEnabled(enabled); } // FIXME: after releasing the old path, all downloads created on the folder becomes inaccessible @@ -198,33 +171,31 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { } String key = preference.getKey(); + int request; - if (key.equals(DOWNLOAD_PATH_VIDEO_PREFERENCE) || key.equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) { - boolean safPick = !usingJavaIO(); - - int request = 0; - if (key.equals(DOWNLOAD_PATH_VIDEO_PREFERENCE)) { - request = REQUEST_DOWNLOAD_VIDEO_PATH; - } else if (key.equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) { - request = REQUEST_DOWNLOAD_AUDIO_PATH; - } - - Intent i; - if (safPick && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) - .putExtra("android.content.extra.SHOW_ADVANCED", true) - .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | StoredDirectoryHelper.PERMISSION_FLAGS); - } else { - i = new Intent(getActivity(), FilePickerActivityHelper.class) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) - .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true) - .putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_DIR); - } - - startActivityForResult(i, request); + if (key.equals(DOWNLOAD_PATH_VIDEO_PREFERENCE)) { + request = REQUEST_DOWNLOAD_VIDEO_PATH; + } else if (key.equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) { + request = REQUEST_DOWNLOAD_AUDIO_PATH; + } else { + return super.onPreferenceTreeClick(preference); } - return super.onPreferenceTreeClick(preference); + Intent i; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + .putExtra("android.content.extra.SHOW_ADVANCED", true) + .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | StoredDirectoryHelper.PERMISSION_FLAGS); + } else { + i = new Intent(getActivity(), FilePickerActivityHelper.class) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true) + .putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_DIR); + } + + startActivityForResult(i, request); + + return true; } @Override @@ -252,7 +223,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment { return; } - if (!usingJavaIO() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // steps: // 1. revoke permissions on the old save path // 2. acquire permissions on the new save path diff --git a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java index f153cf23a..f18d90a95 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java +++ b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java @@ -94,24 +94,7 @@ public class NewPipeSettings { return new File(Environment.getExternalStorageDirectory(), defaultDirectoryName); } - public static void resetDownloadFolders(Context context) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - - prefs.edit() - .putString(context.getString(R.string.downloads_storage_api), context.getString(R.string.downloads_storage_api_default)) - .apply(); - - resetDownloadFolder(prefs, context.getString(R.string.download_path_audio_key), Environment.DIRECTORY_MUSIC); - resetDownloadFolder(prefs, context.getString(R.string.download_path_video_key), Environment.DIRECTORY_MOVIES); - } - - private static void resetDownloadFolder(SharedPreferences prefs, String key, String defaultDirectoryName) { - SharedPreferences.Editor spEditor = prefs.edit(); - spEditor.putString(key, getNewPipeChildFolderPathForDir(getDir(defaultDirectoryName))); - spEditor.apply(); - } - private static String getNewPipeChildFolderPathForDir(File dir) { - return new File(dir, "NewPipe").getAbsolutePath(); + return new File(dir, "NewPipe").toURI().toString(); } } diff --git a/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java b/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java index eb3c9b817..a65c4dff3 100644 --- a/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java +++ b/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java @@ -10,7 +10,6 @@ import android.os.Build; import android.provider.DocumentsContract; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.support.annotation.RequiresApi; import android.support.v4.provider.DocumentFile; import java.io.File; @@ -33,7 +32,6 @@ public class StoredDirectoryHelper { private String tag; - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) public StoredDirectoryHelper(@NonNull Context context, @NonNull Uri path, String tag) throws IOException { this.tag = tag; @@ -50,6 +48,9 @@ public class StoredDirectoryHelper { throw new IOException(e); } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) + throw new IOException("Storage Access Framework with Directory API is not available"); + this.docTree = DocumentFile.fromTreeUri(context, path); if (this.docTree == null) diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java index 8d838ccc2..f25147507 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -1,12 +1,12 @@ package us.shandian.giga.service; import android.Manifest; +import android.app.AlertDialog; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.BroadcastReceiver; -import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -20,7 +20,6 @@ import android.net.NetworkRequest; import android.net.Uri; import android.os.Binder; import android.os.Build; -import android.os.Environment; import android.os.Handler; import android.os.IBinder; import android.os.Looper; @@ -28,6 +27,7 @@ import android.os.Message; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.StringRes; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat.Builder; import android.support.v4.content.PermissionChecker; @@ -41,7 +41,6 @@ import org.schabi.newpipe.player.helper.LockManager; import java.io.File; import java.io.IOException; -import java.net.URI; import java.util.ArrayList; import us.shandian.giga.get.DownloadMission; @@ -141,7 +140,7 @@ public class DownloadManagerService extends Service { mPrefs = PreferenceManager.getDefaultSharedPreferences(this); - mManager = new DownloadManager(this, mHandler, getVideoStorage(), getAudioStorage()); + mManager = new DownloadManager(this, mHandler, loadMainVideoStorage(), loadMainAudioStorage()); Intent openDownloadListIntent = new Intent(this, DownloadActivity.class) .setAction(Intent.ACTION_MAIN); @@ -271,6 +270,33 @@ public class DownloadManagerService extends Service { Toast.makeText(this, "Permission denied (write)", Toast.LENGTH_SHORT).show(); } + // Check download save paths + + String msg = ""; + if (mManager.mMainStorageVideo == null) + msg += getString(R.string.download_path_title); + else if (mManager.mMainStorageAudio == null) + msg += getString(R.string.download_path_audio_title); + + if (!msg.isEmpty()) { + String title; + if (mManager.mMainStorageVideo == null && mManager.mMainStorageAudio == null) { + title = getString(R.string.general_error); + msg = getString(R.string.no_available_dir) + ":\n" + msg; + } else { + title = msg; + msg = getString(R.string.no_available_dir); + } + + new AlertDialog.Builder(this) + .setPositiveButton(android.R.string.ok, null) + .setTitle(title) + .setMessage(msg) + .create() + .show(); + } + + return mBinder; } @@ -348,13 +374,10 @@ public class DownloadManagerService extends Service { mManager.mPrefMeteredDownloads = prefs.getBoolean(key, false); } else if (key.equals(getString(R.string.downloads_queue_limit))) { mManager.mPrefQueueLimit = prefs.getBoolean(key, true); - } else if (key.equals(getString(R.string.downloads_storage_api))) { - mManager.mMainStorageVideo = loadMainStorage(getString(R.string.download_path_video_key), DownloadManager.TAG_VIDEO); - mManager.mMainStorageAudio = loadMainStorage(getString(R.string.download_path_audio_key), DownloadManager.TAG_AUDIO); } else if (key.equals(getString(R.string.download_path_video_key))) { - mManager.mMainStorageVideo = loadMainStorage(key, DownloadManager.TAG_VIDEO); + mManager.mMainStorageVideo = loadMainVideoStorage(); } else if (key.equals(getString(R.string.download_path_audio_key))) { - mManager.mMainStorageAudio = loadMainStorage(key, DownloadManager.TAG_AUDIO); + mManager.mMainStorageAudio = loadMainAudioStorage(); } } @@ -385,7 +408,7 @@ public class DownloadManagerService extends Service { * @param psArgs the arguments for the post-processing algorithm. * @param nearLength the approximated final length of the file */ - public static void startMission(Context context, String urls[], StoredFileHelper storage, char kind, + public static void startMission(Context context, String[] urls, StoredFileHelper storage, char kind, int threads, String source, String psName, String[] psArgs, long nearLength) { Intent intent = new Intent(context, DownloadManagerService.class); intent.setAction(Intent.ACTION_RUN); @@ -538,56 +561,28 @@ public class DownloadManagerService extends Service { mLockAcquired = acquire; } - private StoredDirectoryHelper getVideoStorage() { - return loadMainStorage(getString(R.string.download_path_video_key), DownloadManager.TAG_VIDEO); + private StoredDirectoryHelper loadMainVideoStorage() { + return loadMainStorage(R.string.download_path_video_key, DownloadManager.TAG_VIDEO); } - private StoredDirectoryHelper getAudioStorage() { - return loadMainStorage(getString(R.string.download_path_audio_key), DownloadManager.TAG_AUDIO); + private StoredDirectoryHelper loadMainAudioStorage() { + return loadMainStorage(R.string.download_path_audio_key, DownloadManager.TAG_AUDIO); } + private StoredDirectoryHelper loadMainStorage(@StringRes int prefKey, String tag) { + String path = mPrefs.getString(getString(prefKey), null); - private StoredDirectoryHelper loadMainStorage(String prefKey, String tag) { - String path = mPrefs.getString(prefKey, null); - - final String JAVA_IO = getString(R.string.downloads_storage_api_default); - boolean useJavaIO = JAVA_IO.equals(mPrefs.getString(getString(R.string.downloads_storage_api), JAVA_IO)); - - final String defaultPath; - switch (tag) { - case DownloadManager.TAG_VIDEO: - defaultPath = Environment.DIRECTORY_MOVIES; - break; - case DownloadManager.TAG_AUDIO: - defaultPath = Environment.DIRECTORY_MUSIC; - break; - default: - return null; - } - - if (path == null || path.isEmpty()) { - if (useJavaIO) - return new StoredDirectoryHelper(new File(defaultPath).toURI(), tag); - else - return null; - } + if (path == null || path.isEmpty()) return null; if (path.charAt(0) == File.separatorChar) { - Log.i(TAG, "Migrating old save path: " + path); + Log.i(TAG, "Old save path style present: " + path); - useJavaIO = true; - path = Uri.fromFile(new File(path)).toString(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) + path = Uri.fromFile(new File(path)).toString(); + else + path = ""; - mPrefs.edit().putString(prefKey, path).apply(); - } - - boolean override = path.startsWith(ContentResolver.SCHEME_FILE) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; - if (useJavaIO || override) { - return new StoredDirectoryHelper(URI.create(path), tag); - } - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - return null;// SAF Directory API is not available in older versions + mPrefs.edit().putString(getString(prefKey), "").apply(); } try { @@ -619,6 +614,13 @@ public class DownloadManagerService extends Service { return mManager.mMainStorageAudio; } + public boolean askForSavePath() { + return DownloadManagerService.this.mPrefs.getBoolean( + DownloadManagerService.this.getString(R.string.downloads_storage_ask), + false + ); + } + public void addMissionEventListener(Handler handler) { manageObservers(handler, true); } diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 2e8c62bce..890755845 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -459,19 +459,15 @@ abrir en modo popup Se perdió el progreso porque el archivo fue eliminado Tiempo de espera excedido - API de almacenamiento - Seleccione que API utilizar para almacenar las descargas - - Framework de acceso a almacenamiento - Java I/O - - Guardar como… - No es posible descargar a una tarjeta SD externa. \¿Restablecer la ubicación de la carpeta de descarga\? Seleccione los directorios de descarga Pendiente + Preguntar dónde descargar + Se preguntará dónde guardar cada descarga + Se preguntará dónde guardar cada descarga.\nHabilita esta opción si quieres descargar en la tarjeta SD externa + Desuscribirse Nueva pestaña Elige la pestaña diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 42df857c1..ec288bb18 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -160,20 +160,7 @@ clear_play_history clear_search_history - downloads_storage_api - - - javaIO - - - SAF - javaIO - - - - @string/storage_access_framework_description - @string/java_io_description - + downloads_storage_ask file_rename_charset diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f10a9f589..fb154313e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -550,14 +550,10 @@ Start downloads Pause downloads - Storage API - Select which API use to store the downloads - - Storage Access Framework - Java I/O - - Save as… - Select the downloads save path + Ask where to download + You will be asked where to save each download + You will be asked where to save each download.\nEnable this option if you want download to the external SD Card + \ No newline at end of file diff --git a/app/src/main/res/xml/download_settings.xml b/app/src/main/res/xml/download_settings.xml index 2f62aa89e..7a6fab841 100644 --- a/app/src/main/res/xml/download_settings.xml +++ b/app/src/main/res/xml/download_settings.xml @@ -5,14 +5,12 @@ android:title="@string/settings_category_downloads_title"> - + android:defaultValue="false" + android:key="@string/downloads_storage_ask" + android:summary="@string/downloads_storage_ask_summary_kitkat" + android:title="@string/downloads_storage_ask_title" /> Date: Thu, 25 Apr 2019 00:34:29 -0300 Subject: [PATCH 135/138] Space reserving tweaks for huge video resolutions * improve space reserving, allows write better 4K/8K video data * do not use cache dirs in the muxers, Android can force close NewPipe if the device is running out of storage. Is a aggressive cache cleaning >:/ * (for devs) webm & mkv are the same thing * calculate the final file size inside of the mission, instead getting from the UI * simplify ps algorithms constructors * [missing old commit message] simplify the loading of pending downloads --- .../newpipe/download/DownloadDialog.java | 5 +- .../giga/get/DownloadInitializer.java | 349 ++++++------ .../us/shandian/giga/get/DownloadMission.java | 30 +- .../giga/postprocessing/M4aNoDash.java | 4 +- .../giga/postprocessing/Mp4FromDashMuxer.java | 2 +- .../giga/postprocessing/Postprocessing.java | 503 +++++++++--------- .../giga/postprocessing/TtmlConverter.java | 144 ++--- .../giga/postprocessing/WebMMuxer.java | 88 +-- .../giga/service/DownloadManager.java | 25 +- .../giga/service/DownloadManagerService.java | 5 +- .../java/us/shandian/giga/util/Utility.java | 3 +- 11 files changed, 608 insertions(+), 550 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index f27e7467e..8fef9a995 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -767,7 +767,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck psArgs = null; long videoSize = wrappedVideoStreams.getSizeInBytes((VideoStream) selectedStream); - // set nearLength, only, if both sizes are fetched or known. this probably does not work on slow networks + // set nearLength, only, if both sizes are fetched or known. This probably + // does not work on slow networks but is later updated in the downloader if (secondaryStream.getSizeInBytes() > 0 && videoSize > 0) { nearLength = secondaryStream.getSizeInBytes() + videoSize; } @@ -793,7 +794,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck if (secondaryStreamUrl == null) { urls = new String[]{selectedStream.getUrl()}; } else { - urls = new String[]{secondaryStreamUrl, selectedStream.getUrl()}; + urls = new String[]{selectedStream.getUrl(), secondaryStreamUrl}; } DownloadManagerService.startMission(context, urls, storage, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength); diff --git a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java index 590c3704c..5239c5bb7 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java @@ -1,158 +1,191 @@ -package us.shandian.giga.get; - -import android.support.annotation.NonNull; -import android.util.Log; - -import org.schabi.newpipe.streams.io.SharpStream; - -import java.io.IOException; -import java.io.InterruptedIOException; -import java.net.HttpURLConnection; -import java.nio.channels.ClosedByInterruptException; - -import us.shandian.giga.util.Utility; - -import static org.schabi.newpipe.BuildConfig.DEBUG; - -public class DownloadInitializer extends Thread { - private final static String TAG = "DownloadInitializer"; - final static int mId = 0; - - private DownloadMission mMission; - private HttpURLConnection mConn; - - DownloadInitializer(@NonNull DownloadMission mission) { - mMission = mission; - mConn = null; - } - - @Override - public void run() { - if (mMission.current > 0) mMission.resetState(false, true, DownloadMission.ERROR_NOTHING); - - int retryCount = 0; - while (true) { - try { - mMission.currentThreadCount = mMission.threadCount; - - mConn = mMission.openConnection(mId, -1, -1); - mMission.establishConnection(mId, mConn); - - if (!mMission.running || Thread.interrupted()) return; - - mMission.length = Utility.getContentLength(mConn); - - - if (mMission.length == 0) { - mMission.notifyError(DownloadMission.ERROR_HTTP_NO_CONTENT, null); - return; - } - - // check for dynamic generated content - if (mMission.length == -1 && mConn.getResponseCode() == 200) { - mMission.blocks = 0; - mMission.length = 0; - mMission.fallback = true; - mMission.unknownLength = true; - mMission.currentThreadCount = 1; - - if (DEBUG) { - Log.d(TAG, "falling back (unknown length)"); - } - } else { - // Open again - mConn = mMission.openConnection(mId, mMission.length - 10, mMission.length); - mMission.establishConnection(mId, mConn); - - if (!mMission.running || Thread.interrupted()) return; - - synchronized (mMission.blockState) { - if (mConn.getResponseCode() == 206) { - if (mMission.currentThreadCount > 1) { - mMission.blocks = mMission.length / DownloadMission.BLOCK_SIZE; - - if (mMission.currentThreadCount > mMission.blocks) { - mMission.currentThreadCount = (int) mMission.blocks; - } - if (mMission.currentThreadCount <= 0) { - mMission.currentThreadCount = 1; - } - if (mMission.blocks * DownloadMission.BLOCK_SIZE < mMission.length) { - mMission.blocks++; - } - } else { - // if one thread is solicited don't calculate blocks, is useless - mMission.blocks = 1; - mMission.fallback = true; - mMission.unknownLength = false; - } - - if (DEBUG) { - Log.d(TAG, "http response code = " + mConn.getResponseCode()); - } - } else { - // Fallback to single thread - mMission.blocks = 0; - mMission.fallback = true; - mMission.unknownLength = false; - mMission.currentThreadCount = 1; - - if (DEBUG) { - Log.d(TAG, "falling back due http response code = " + mConn.getResponseCode()); - } - } - - for (long i = 0; i < mMission.currentThreadCount; i++) { - mMission.threadBlockPositions.add(i); - mMission.threadBytePositions.add(0L); - } - } - - if (!mMission.running || Thread.interrupted()) return; - } - - SharpStream fs = mMission.storage.getStream(); - fs.setLength(mMission.offsets[mMission.current] + mMission.length); - fs.seek(mMission.offsets[mMission.current]); - fs.close(); - - if (!mMission.running || Thread.interrupted()) return; - - mMission.running = false; - break; - } catch (InterruptedIOException | ClosedByInterruptException e) { - return; - } catch (Exception e) { - if (!mMission.running) return; - - if (e instanceof IOException && e.getMessage().contains("Permission denied")) { - mMission.notifyError(DownloadMission.ERROR_PERMISSION_DENIED, e); - return; - } - - if (retryCount++ > mMission.maxRetry) { - Log.e(TAG, "initializer failed", e); - mMission.notifyError(e); - return; - } - - Log.e(TAG, "initializer failed, retrying", e); - } - } - - mMission.start(); - } - - @Override - public void interrupt() { - super.interrupt(); - - if (mConn != null) { - try { - mConn.disconnect(); - } catch (Exception e) { - // nothing to do - } - } - } -} +package us.shandian.giga.get; + +import android.support.annotation.NonNull; +import android.util.Log; + +import org.schabi.newpipe.streams.io.SharpStream; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.HttpURLConnection; +import java.nio.channels.ClosedByInterruptException; + +import us.shandian.giga.util.Utility; + +import static org.schabi.newpipe.BuildConfig.DEBUG; + +public class DownloadInitializer extends Thread { + private final static String TAG = "DownloadInitializer"; + final static int mId = 0; + private final static int RESERVE_SPACE_DEFAULT = 5 * 1024 * 1024;// 5 MiB + private final static int RESERVE_SPACE_MAXIMUM = 150 * 1024 * 1024;// 150 MiB + + private DownloadMission mMission; + private HttpURLConnection mConn; + + DownloadInitializer(@NonNull DownloadMission mission) { + mMission = mission; + mConn = null; + } + + @Override + public void run() { + if (mMission.current > 0) mMission.resetState(false, true, DownloadMission.ERROR_NOTHING); + + int retryCount = 0; + while (true) { + try { + mMission.currentThreadCount = mMission.threadCount; + + if (mMission.blocks < 0 && mMission.current == 0) { + // calculate the whole size of the mission + long finalLength = 0; + long lowestSize = Long.MAX_VALUE; + + for (int i = 0; i < mMission.urls.length && mMission.running; i++) { + mConn = mMission.openConnection(mMission.urls[i], mId, -1, -1); + mMission.establishConnection(mId, mConn); + + if (Thread.interrupted()) return; + long length = Utility.getContentLength(mConn); + + if (i == 0) mMission.length = length; + if (length > 0) finalLength += length; + if (length < lowestSize) lowestSize = length; + } + + mMission.nearLength = finalLength; + + // reserve space at the start of the file + if (mMission.psAlgorithm != null && mMission.psAlgorithm.reserveSpace) { + if (lowestSize < 1) { + // the length is unknown use the default size + mMission.offsets[0] = RESERVE_SPACE_DEFAULT; + } else { + // use the smallest resource size to download, otherwise, use the maximum + mMission.offsets[0] = lowestSize < RESERVE_SPACE_MAXIMUM ? lowestSize : RESERVE_SPACE_MAXIMUM; + } + } + } else { + // ask for the current resource length + mConn = mMission.openConnection(mId, -1, -1); + mMission.establishConnection(mId, mConn); + + if (!mMission.running || Thread.interrupted()) return; + + mMission.length = Utility.getContentLength(mConn); + } + + if (mMission.length == 0 || mConn.getResponseCode() == 204) { + mMission.notifyError(DownloadMission.ERROR_HTTP_NO_CONTENT, null); + return; + } + + // check for dynamic generated content + if (mMission.length == -1 && mConn.getResponseCode() == 200) { + mMission.blocks = 0; + mMission.length = 0; + mMission.fallback = true; + mMission.unknownLength = true; + mMission.currentThreadCount = 1; + + if (DEBUG) { + Log.d(TAG, "falling back (unknown length)"); + } + } else { + // Open again + mConn = mMission.openConnection(mId, mMission.length - 10, mMission.length); + mMission.establishConnection(mId, mConn); + + if (!mMission.running || Thread.interrupted()) return; + + synchronized (mMission.blockState) { + if (mConn.getResponseCode() == 206) { + if (mMission.currentThreadCount > 1) { + mMission.blocks = mMission.length / DownloadMission.BLOCK_SIZE; + + if (mMission.currentThreadCount > mMission.blocks) { + mMission.currentThreadCount = (int) mMission.blocks; + } + if (mMission.currentThreadCount <= 0) { + mMission.currentThreadCount = 1; + } + if (mMission.blocks * DownloadMission.BLOCK_SIZE < mMission.length) { + mMission.blocks++; + } + } else { + // if one thread is solicited don't calculate blocks, is useless + mMission.blocks = 1; + mMission.fallback = true; + mMission.unknownLength = false; + } + + if (DEBUG) { + Log.d(TAG, "http response code = " + mConn.getResponseCode()); + } + } else { + // Fallback to single thread + mMission.blocks = 0; + mMission.fallback = true; + mMission.unknownLength = false; + mMission.currentThreadCount = 1; + + if (DEBUG) { + Log.d(TAG, "falling back due http response code = " + mConn.getResponseCode()); + } + } + + for (long i = 0; i < mMission.currentThreadCount; i++) { + mMission.threadBlockPositions.add(i); + mMission.threadBytePositions.add(0L); + } + } + + if (!mMission.running || Thread.interrupted()) return; + } + + SharpStream fs = mMission.storage.getStream(); + fs.setLength(mMission.offsets[mMission.current] + mMission.length); + fs.seek(mMission.offsets[mMission.current]); + fs.close(); + + if (!mMission.running || Thread.interrupted()) return; + + mMission.running = false; + break; + } catch (InterruptedIOException | ClosedByInterruptException e) { + return; + } catch (Exception e) { + if (!mMission.running) return; + + if (e instanceof IOException && e.getMessage().contains("Permission denied")) { + mMission.notifyError(DownloadMission.ERROR_PERMISSION_DENIED, e); + return; + } + + if (retryCount++ > mMission.maxRetry) { + Log.e(TAG, "initializer failed", e); + mMission.notifyError(e); + return; + } + + Log.e(TAG, "initializer failed, retrying", e); + } + } + + mMission.start(); + } + + @Override + public void interrupt() { + super.interrupt(); + + if (mConn != null) { + try { + mConn.disconnect(); + } catch (Exception e) { + // nothing to do + } + } + } +} diff --git a/app/src/main/java/us/shandian/giga/get/DownloadMission.java b/app/src/main/java/us/shandian/giga/get/DownloadMission.java index 9bc46e3af..b3e32a43c 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMission.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMission.java @@ -147,14 +147,10 @@ public class DownloadMission extends Mission { this.enqueued = true; this.maxRetry = 3; this.storage = storage; + this.psAlgorithm = psInstance; - if (psInstance != null) { - this.psAlgorithm = psInstance; - this.offsets[0] = psInstance.recommendedReserve; - } else { - if (DEBUG && urls.length > 1) { - Log.w(TAG, "mission created with multiple urls ¿missing post-processing algorithm?"); - } + if (DEBUG && psInstance == null && urls.length > 1) { + Log.w(TAG, "mission created with multiple urls ¿missing post-processing algorithm?"); } } @@ -233,10 +229,14 @@ public class DownloadMission extends Mission { * @throws IOException if an I/O exception occurs. */ HttpURLConnection openConnection(int threadId, long rangeStart, long rangeEnd) throws IOException { - URL url = new URL(urls[current]); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + return openConnection(urls[current], threadId, rangeStart, rangeEnd); + } + + HttpURLConnection openConnection(String url, int threadId, long rangeStart, long rangeEnd) throws IOException { + HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); conn.setInstanceFollowRedirects(true); conn.setRequestProperty("User-Agent", Downloader.USER_AGENT); + conn.setRequestProperty("Accept", "*/*"); // BUG workaround: switching between networks can freeze the download forever conn.setConnectTimeout(30000); @@ -536,8 +536,11 @@ public class DownloadMission extends Mission { @Override public boolean delete() { deleted = true; + if (psAlgorithm != null) psAlgorithm.cleanupTemporalDir(); + boolean res = deleteThisFromFile(); - if (!super.delete()) res = false; + + if (!super.delete()) return false; return res; } @@ -626,6 +629,11 @@ public class DownloadMission extends Mission { return blocks >= 0; // DownloadMissionInitializer was executed } + /** + * Gets the approximated final length of the file + * + * @return the length in bytes + */ public long getLength() { long calculated; if (psState == 1 || psState == 3) { @@ -681,6 +689,8 @@ public class DownloadMission extends Mission { private boolean doPostprocessing() { if (psAlgorithm == null || psState == 2) return true; + errObject = null; + notifyPostProcessing(1); notifyProgress(0); diff --git a/app/src/main/java/us/shandian/giga/postprocessing/M4aNoDash.java b/app/src/main/java/us/shandian/giga/postprocessing/M4aNoDash.java index ee7e4cba1..aa5170908 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/M4aNoDash.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/M4aNoDash.java @@ -6,10 +6,10 @@ import org.schabi.newpipe.streams.io.SharpStream; import java.io.IOException; -public class M4aNoDash extends Postprocessing { +class M4aNoDash extends Postprocessing { M4aNoDash() { - super(0, true); + super(false, true, ALGORITHM_M4A_NO_DASH); } @Override diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Mp4FromDashMuxer.java b/app/src/main/java/us/shandian/giga/postprocessing/Mp4FromDashMuxer.java index f12c1c2d2..74cb43116 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/Mp4FromDashMuxer.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/Mp4FromDashMuxer.java @@ -11,7 +11,7 @@ import java.io.IOException; class Mp4FromDashMuxer extends Postprocessing { Mp4FromDashMuxer() { - super(3 * 1024 * 1024/* 3 MiB */, true); + super(true, true, ALGORITHM_MP4_FROM_DASH_MUXER); } @Override diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java index 3d10628e7..15c4f575d 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java @@ -1,247 +1,256 @@ -package us.shandian.giga.postprocessing; - -import android.os.Message; -import android.support.annotation.NonNull; -import android.util.Log; - -import org.schabi.newpipe.streams.io.SharpStream; - -import java.io.File; -import java.io.IOException; -import java.io.Serializable; - -import us.shandian.giga.get.DownloadMission; -import us.shandian.giga.io.ChunkFileInputStream; -import us.shandian.giga.io.CircularFileWriter; -import us.shandian.giga.io.CircularFileWriter.OffsetChecker; -import us.shandian.giga.service.DownloadManagerService; - -import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING; -import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD; -import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION; - -public abstract class Postprocessing implements Serializable { - - static transient final byte OK_RESULT = ERROR_NOTHING; - - public transient static final String ALGORITHM_TTML_CONVERTER = "ttml"; - public transient static final String ALGORITHM_WEBM_MUXER = "webm"; - public transient static final String ALGORITHM_MP4_FROM_DASH_MUXER = "mp4D-mp4"; - public transient static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a"; - - public static Postprocessing getAlgorithm(@NonNull String algorithmName, String[] args, @NonNull File cacheDir) { - Postprocessing instance; - - switch (algorithmName) { - case ALGORITHM_TTML_CONVERTER: - instance = new TtmlConverter(); - break; - case ALGORITHM_WEBM_MUXER: - instance = new WebMMuxer(); - break; - case ALGORITHM_MP4_FROM_DASH_MUXER: - instance = new Mp4FromDashMuxer(); - break; - case ALGORITHM_M4A_NO_DASH: - instance = new M4aNoDash(); - break; - /*case "example-algorithm": - instance = new ExampleAlgorithm();*/ - default: - throw new RuntimeException("Unimplemented post-processing algorithm: " + algorithmName); - } - - instance.args = args; - instance.name = algorithmName;// for debug only, maybe remove this field in the future - instance.cacheDir = cacheDir; - - return instance; - } - - /** - * Get a boolean value that indicate if the given algorithm work on the same - * file - */ - public boolean worksOnSameFile; - - /** - * Get the recommended space to reserve for the given algorithm. The amount - * is in bytes - */ - public int recommendedReserve; - - /** - * the download to post-process - */ - protected transient DownloadMission mission; - - public transient File cacheDir; - - private String[] args; - - private String name; - - Postprocessing(int recommendedReserve, boolean worksOnSameFile) { - this.recommendedReserve = recommendedReserve; - this.worksOnSameFile = worksOnSameFile; - } - - public void run(DownloadMission target) throws IOException { - this.mission = target; - - File temp = null; - CircularFileWriter out = null; - int result; - long finalLength = -1; - - mission.done = 0; - mission.length = mission.storage.length(); - - if (worksOnSameFile) { - ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length]; - try { - int i = 0; - for (; i < sources.length - 1; i++) { - sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i], mission.offsets[i + 1]); - } - sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i]); - - if (test(sources)) { - for (SharpStream source : sources) source.rewind(); - - OffsetChecker checker = () -> { - for (ChunkFileInputStream source : sources) { - /* - * WARNING: never use rewind() in any chunk after any writing (especially on first chunks) - * or the CircularFileWriter can lead to unexpected results - */ - if (source.isClosed() || source.available() < 1) { - continue;// the selected source is not used anymore - } - - return source.getFilePointer() - 1; - } - - return -1; - }; - - temp = new File(cacheDir, mission.storage.getName() + ".tmp"); - - out = new CircularFileWriter(mission.storage.getStream(), temp, checker); - out.onProgress = this::progressReport; - - out.onWriteError = (err) -> { - mission.psState = 3; - mission.notifyError(ERROR_POSTPROCESSING_HOLD, err); - - try { - synchronized (this) { - while (mission.psState == 3) - wait(); - } - } catch (InterruptedException e) { - // nothing to do - Log.e(this.getClass().getSimpleName(), "got InterruptedException"); - } - - return mission.errCode == ERROR_NOTHING; - }; - - result = process(out, sources); - - if (result == OK_RESULT) - finalLength = out.finalizeFile(); - } else { - result = OK_RESULT; - } - } finally { - for (SharpStream source : sources) { - if (source != null && !source.isClosed()) { - source.close(); - } - } - if (out != null) { - out.close(); - } - if (temp != null) { - //noinspection ResultOfMethodCallIgnored - temp.delete(); - } - } - } else { - result = test() ? process(null) : OK_RESULT; - } - - if (result == OK_RESULT) { - if (finalLength != -1) { - mission.done = finalLength; - mission.length = finalLength; - } - } else { - mission.errCode = ERROR_UNKNOWN_EXCEPTION; - mission.errObject = new RuntimeException("post-processing algorithm returned " + result); - } - - if (result != OK_RESULT && worksOnSameFile) mission.storage.delete(); - - this.mission = null; - } - - /** - * Test if the post-processing algorithm can be skipped - * - * @param sources files to be processed - * @return {@code true} if the post-processing is required, otherwise, {@code false} - * @throws IOException if an I/O error occurs. - */ - boolean test(SharpStream... sources) throws IOException { - return true; - } - - /** - * Abstract method to execute the post-processing algorithm - * - * @param out output stream - * @param sources files to be processed - * @return a error code, 0 means the operation was successful - * @throws IOException if an I/O error occurs. - */ - abstract int process(SharpStream out, SharpStream... sources) throws IOException; - - String getArgumentAt(int index, String defaultValue) { - if (args == null || index >= args.length) { - return defaultValue; - } - - return args[index]; - } - - private void progressReport(long done) { - mission.done = done; - if (mission.length < mission.done) mission.length = mission.done; - - Message m = new Message(); - m.what = DownloadManagerService.MESSAGE_PROGRESS; - m.obj = mission; - - mission.mHandler.sendMessage(m); - } - - @NonNull - @Override - public String toString() { - StringBuilder str = new StringBuilder(); - - str.append("name=").append(name).append('['); - - if (args != null) { - for (String arg : args) { - str.append(", "); - str.append(arg); - } - str.delete(0, 1); - } - - return str.append(']').toString(); - } -} +package us.shandian.giga.postprocessing; + +import android.os.Message; +import android.support.annotation.NonNull; +import android.util.Log; + +import org.schabi.newpipe.streams.io.SharpStream; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; + +import us.shandian.giga.get.DownloadMission; +import us.shandian.giga.io.ChunkFileInputStream; +import us.shandian.giga.io.CircularFileWriter; +import us.shandian.giga.io.CircularFileWriter.OffsetChecker; +import us.shandian.giga.service.DownloadManagerService; + +import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING; +import static us.shandian.giga.get.DownloadMission.ERROR_POSTPROCESSING_HOLD; +import static us.shandian.giga.get.DownloadMission.ERROR_UNKNOWN_EXCEPTION; + +public abstract class Postprocessing implements Serializable { + + static transient final byte OK_RESULT = ERROR_NOTHING; + + public transient static final String ALGORITHM_TTML_CONVERTER = "ttml"; + public transient static final String ALGORITHM_WEBM_MUXER = "webm"; + public transient static final String ALGORITHM_MP4_FROM_DASH_MUXER = "mp4D-mp4"; + public transient static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a"; + + public static Postprocessing getAlgorithm(@NonNull String algorithmName, String[] args) { + Postprocessing instance; + + switch (algorithmName) { + case ALGORITHM_TTML_CONVERTER: + instance = new TtmlConverter(); + break; + case ALGORITHM_WEBM_MUXER: + instance = new WebMMuxer(); + break; + case ALGORITHM_MP4_FROM_DASH_MUXER: + instance = new Mp4FromDashMuxer(); + break; + case ALGORITHM_M4A_NO_DASH: + instance = new M4aNoDash(); + break; + /*case "example-algorithm": + instance = new ExampleAlgorithm();*/ + default: + throw new UnsupportedOperationException("Unimplemented post-processing algorithm: " + algorithmName); + } + + instance.args = args; + return instance; + } + + /** + * Get a boolean value that indicate if the given algorithm work on the same + * file + */ + public final boolean worksOnSameFile; + + /** + * Indicates whether the selected algorithm needs space reserved at the beginning of the file + */ + public final boolean reserveSpace; + + /** + * Gets the given algorithm short name + */ + private final String name; + + + private String[] args; + + protected transient DownloadMission mission; + + private File tempFile; + + Postprocessing(boolean reserveSpace, boolean worksOnSameFile, String algorithmName) { + this.reserveSpace = reserveSpace; + this.worksOnSameFile = worksOnSameFile; + this.name = algorithmName;// for debugging only + } + + public void setTemporalDir(@NonNull File directory) { + long rnd = (int) (Math.random() * 100000f); + tempFile = new File(directory, rnd + "_" + System.nanoTime() + ".tmp"); + } + + public void cleanupTemporalDir() { + if (tempFile != null && tempFile.exists()) { + //noinspection ResultOfMethodCallIgnored + tempFile.delete(); + } + } + + + public void run(DownloadMission target) throws IOException { + this.mission = target; + + CircularFileWriter out = null; + int result; + long finalLength = -1; + + mission.done = 0; + mission.length = mission.storage.length(); + + if (worksOnSameFile) { + ChunkFileInputStream[] sources = new ChunkFileInputStream[mission.urls.length]; + try { + int i = 0; + for (; i < sources.length - 1; i++) { + sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i], mission.offsets[i + 1]); + } + sources[i] = new ChunkFileInputStream(mission.storage.getStream(), mission.offsets[i]); + + if (test(sources)) { + for (SharpStream source : sources) source.rewind(); + + OffsetChecker checker = () -> { + for (ChunkFileInputStream source : sources) { + /* + * WARNING: never use rewind() in any chunk after any writing (especially on first chunks) + * or the CircularFileWriter can lead to unexpected results + */ + if (source.isClosed() || source.available() < 1) { + continue;// the selected source is not used anymore + } + + return source.getFilePointer() - 1; + } + + return -1; + }; + + out = new CircularFileWriter(mission.storage.getStream(), tempFile, checker); + out.onProgress = this::progressReport; + + out.onWriteError = (err) -> { + mission.psState = 3; + mission.notifyError(ERROR_POSTPROCESSING_HOLD, err); + + try { + synchronized (this) { + while (mission.psState == 3) + wait(); + } + } catch (InterruptedException e) { + // nothing to do + Log.e(this.getClass().getSimpleName(), "got InterruptedException"); + } + + return mission.errCode == ERROR_NOTHING; + }; + + result = process(out, sources); + + if (result == OK_RESULT) + finalLength = out.finalizeFile(); + } else { + result = OK_RESULT; + } + } finally { + for (SharpStream source : sources) { + if (source != null && !source.isClosed()) { + source.close(); + } + } + if (out != null) { + out.close(); + } + if (tempFile != null) { + //noinspection ResultOfMethodCallIgnored + tempFile.delete(); + tempFile = null; + } + } + } else { + result = test() ? process(null) : OK_RESULT; + } + + if (result == OK_RESULT) { + if (finalLength != -1) { + mission.done = finalLength; + mission.length = finalLength; + } + } else { + mission.errCode = ERROR_UNKNOWN_EXCEPTION; + mission.errObject = new RuntimeException("post-processing algorithm returned " + result); + } + + if (result != OK_RESULT && worksOnSameFile) mission.storage.delete(); + + this.mission = null; + } + + /** + * Test if the post-processing algorithm can be skipped + * + * @param sources files to be processed + * @return {@code true} if the post-processing is required, otherwise, {@code false} + * @throws IOException if an I/O error occurs. + */ + boolean test(SharpStream... sources) throws IOException { + return true; + } + + /** + * Abstract method to execute the post-processing algorithm + * + * @param out output stream + * @param sources files to be processed + * @return a error code, 0 means the operation was successful + * @throws IOException if an I/O error occurs. + */ + abstract int process(SharpStream out, SharpStream... sources) throws IOException; + + String getArgumentAt(int index, String defaultValue) { + if (args == null || index >= args.length) { + return defaultValue; + } + + return args[index]; + } + + private void progressReport(long done) { + mission.done = done; + if (mission.length < mission.done) mission.length = mission.done; + + Message m = new Message(); + m.what = DownloadManagerService.MESSAGE_PROGRESS; + m.obj = mission; + + mission.mHandler.sendMessage(m); + } + + @NonNull + @Override + public String toString() { + StringBuilder str = new StringBuilder(); + + str.append("name=").append(name).append('['); + + if (args != null) { + for (String arg : args) { + str.append(", "); + str.append(arg); + } + str.delete(0, 1); + } + + return str.append(']').toString(); + } +} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/TtmlConverter.java b/app/src/main/java/us/shandian/giga/postprocessing/TtmlConverter.java index bba0b299a..5a5b687f7 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/TtmlConverter.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/TtmlConverter.java @@ -1,72 +1,72 @@ -package us.shandian.giga.postprocessing; - -import android.util.Log; - -import org.schabi.newpipe.streams.SubtitleConverter; -import org.schabi.newpipe.streams.io.SharpStream; -import org.xml.sax.SAXException; - -import java.io.IOException; -import java.text.ParseException; - -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.xpath.XPathExpressionException; - -/** - * @author kapodamy - */ -class TtmlConverter extends Postprocessing { - private static final String TAG = "TtmlConverter"; - - TtmlConverter() { - // due how XmlPullParser works, the xml is fully loaded on the ram - super(0, true); - } - - @Override - int process(SharpStream out, SharpStream... sources) throws IOException { - // check if the subtitle is already in srt and copy, this should never happen - String format = getArgumentAt(0, null); - - if (format == null || format.equals("ttml")) { - SubtitleConverter ttmlDumper = new SubtitleConverter(); - - try { - ttmlDumper.dumpTTML( - sources[0], - out, - getArgumentAt(1, "true").equals("true"), - getArgumentAt(2, "true").equals("true") - ); - } catch (Exception err) { - Log.e(TAG, "subtitle parse failed", err); - - if (err instanceof IOException) { - return 1; - } else if (err instanceof ParseException) { - return 2; - } else if (err instanceof SAXException) { - return 3; - } else if (err instanceof ParserConfigurationException) { - return 4; - } else if (err instanceof XPathExpressionException) { - return 7; - } - - return 8; - } - - return OK_RESULT; - } else if (format.equals("srt")) { - byte[] buffer = new byte[8 * 1024]; - int read; - while ((read = sources[0].read(buffer)) > 0) { - out.write(buffer, 0, read); - } - return OK_RESULT; - } - - throw new UnsupportedOperationException("Can't convert this subtitle, unimplemented format: " + format); - } - -} +package us.shandian.giga.postprocessing; + +import android.util.Log; + +import org.schabi.newpipe.streams.SubtitleConverter; +import org.schabi.newpipe.streams.io.SharpStream; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.text.ParseException; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPathExpressionException; + +/** + * @author kapodamy + */ +class TtmlConverter extends Postprocessing { + private static final String TAG = "TtmlConverter"; + + TtmlConverter() { + // due how XmlPullParser works, the xml is fully loaded on the ram + super(false, true, ALGORITHM_TTML_CONVERTER); + } + + @Override + int process(SharpStream out, SharpStream... sources) throws IOException { + // check if the subtitle is already in srt and copy, this should never happen + String format = getArgumentAt(0, null); + + if (format == null || format.equals("ttml")) { + SubtitleConverter ttmlDumper = new SubtitleConverter(); + + try { + ttmlDumper.dumpTTML( + sources[0], + out, + getArgumentAt(1, "true").equals("true"), + getArgumentAt(2, "true").equals("true") + ); + } catch (Exception err) { + Log.e(TAG, "subtitle parse failed", err); + + if (err instanceof IOException) { + return 1; + } else if (err instanceof ParseException) { + return 2; + } else if (err instanceof SAXException) { + return 3; + } else if (err instanceof ParserConfigurationException) { + return 4; + } else if (err instanceof XPathExpressionException) { + return 7; + } + + return 8; + } + + return OK_RESULT; + } else if (format.equals("srt")) { + byte[] buffer = new byte[8 * 1024]; + int read; + while ((read = sources[0].read(buffer)) > 0) { + out.write(buffer, 0, read); + } + return OK_RESULT; + } + + throw new UnsupportedOperationException("Can't convert this subtitle, unimplemented format: " + format); + } + +} diff --git a/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java b/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java index 618c1ec5a..ea1676482 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/WebMMuxer.java @@ -1,44 +1,44 @@ -package us.shandian.giga.postprocessing; - -import org.schabi.newpipe.streams.WebMReader.TrackKind; -import org.schabi.newpipe.streams.WebMReader.WebMTrack; -import org.schabi.newpipe.streams.WebMWriter; -import org.schabi.newpipe.streams.io.SharpStream; - -import java.io.IOException; - -/** - * @author kapodamy - */ -class WebMMuxer extends Postprocessing { - - WebMMuxer() { - super(5 * 1024 * 1024/* 5 MiB */, true); - } - - @Override - int process(SharpStream out, SharpStream... sources) throws IOException { - WebMWriter muxer = new WebMWriter(sources); - muxer.parseSources(); - - // youtube uses a webm with a fake video track that acts as a "cover image" - int[] indexes = new int[sources.length]; - - for (int i = 0; i < sources.length; i++) { - WebMTrack[] tracks = muxer.getTracksFromSource(i); - for (int j = 0; j < tracks.length; j++) { - if (tracks[j].kind == TrackKind.Audio) { - indexes[i] = j; - i = sources.length; - break; - } - } - } - - muxer.selectTracks(indexes); - muxer.build(out); - - return OK_RESULT; - } - -} +package us.shandian.giga.postprocessing; + +import org.schabi.newpipe.streams.WebMReader.TrackKind; +import org.schabi.newpipe.streams.WebMReader.WebMTrack; +import org.schabi.newpipe.streams.WebMWriter; +import org.schabi.newpipe.streams.io.SharpStream; + +import java.io.IOException; + +/** + * @author kapodamy + */ +class WebMMuxer extends Postprocessing { + + WebMMuxer() { + super(true, true, ALGORITHM_WEBM_MUXER); + } + + @Override + int process(SharpStream out, SharpStream... sources) throws IOException { + WebMWriter muxer = new WebMWriter(sources); + muxer.parseSources(); + + // youtube uses a webm with a fake video track that acts as a "cover image" + int[] indexes = new int[sources.length]; + + for (int i = 0; i < sources.length; i++) { + WebMTrack[] tracks = muxer.getTracksFromSource(i); + for (int j = 0; j < tracks.length; j++) { + if (tracks[j].kind == TrackKind.Audio) { + indexes[i] = j; + i = sources.length; + break; + } + } + } + + muxer.selectTracks(indexes); + muxer.build(out); + + return OK_RESULT; + } + +} diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManager.java b/app/src/main/java/us/shandian/giga/service/DownloadManager.java index 479c4b92f..c2bba7396 100644 --- a/app/src/main/java/us/shandian/giga/service/DownloadManager.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManager.java @@ -154,7 +154,9 @@ public class DownloadManager { if (mis.psAlgorithm.worksOnSameFile) { // Incomplete post-processing results in a corrupted download file // because the selected algorithm works on the same file to save space. - if (exists && !mis.storage.delete()) + // the file will be deleted if the storage API + // is Java IO (avoid showing the "Save as..." dialog) + if (exists && mis.storage.isDirect() && !mis.storage.delete()) Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath()); exists = true; @@ -162,7 +164,6 @@ public class DownloadManager { mis.psState = 0; mis.errCode = DownloadMission.ERROR_POSTPROCESSING_STOPPED; - mis.errObject = null; } else if (!exists) { tryRecover(mis); @@ -171,8 +172,10 @@ public class DownloadManager { mis.resetState(true, true, DownloadMission.ERROR_PROGRESS_LOST); } - if (mis.psAlgorithm != null) - mis.psAlgorithm.cacheDir = pickAvailableCacheDir(ctx); + if (mis.psAlgorithm != null) { + mis.psAlgorithm.cleanupTemporalDir(); + mis.psAlgorithm.setTemporalDir(pickAvailableTemporalDir(ctx)); + } mis.recovered = exists; mis.metadata = sub; @@ -532,14 +535,14 @@ public class DownloadManager { } private static boolean isDirectoryAvailable(File directory) { - return directory != null && directory.canWrite(); + return directory != null && directory.canWrite() && directory.exists(); } - static File pickAvailableCacheDir(@NonNull Context ctx) { - if (isDirectoryAvailable(ctx.getExternalCacheDir())) - return ctx.getExternalCacheDir(); - else if (isDirectoryAvailable(ctx.getCacheDir())) - return ctx.getCacheDir(); + static File pickAvailableTemporalDir(@NonNull Context ctx) { + if (isDirectoryAvailable(ctx.getExternalFilesDir(null))) + return ctx.getExternalFilesDir(null); + else if (isDirectoryAvailable(ctx.getFilesDir())) + return ctx.getFilesDir(); // this never should happen return ctx.getDir("tmp", Context.MODE_PRIVATE); @@ -550,7 +553,7 @@ public class DownloadManager { if (tag.equals(TAG_AUDIO)) return mMainStorageAudio; if (tag.equals(TAG_VIDEO)) return mMainStorageVideo; - Log.w(TAG, "Unknown download category, not [audio video]: " + String.valueOf(tag)); + Log.w(TAG, "Unknown download category, not [audio video]: " + tag); return null;// this never should happen } diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java index f25147507..aab0257db 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -450,13 +450,16 @@ public class DownloadManagerService extends Service { if (psName == null) ps = null; else - ps = Postprocessing.getAlgorithm(psName, psArgs, DownloadManager.pickAvailableCacheDir(this)); + ps = Postprocessing.getAlgorithm(psName, psArgs); final DownloadMission mission = new DownloadMission(urls, storage, kind, ps); mission.threadCount = threads; mission.source = source; mission.nearLength = nearLength; + if (ps != null) + ps.setTemporalDir(DownloadManager.pickAvailableTemporalDir(this)); + handleConnectivityState(true);// first check the actual network status mManager.startMission(mission); diff --git a/app/src/main/java/us/shandian/giga/util/Utility.java b/app/src/main/java/us/shandian/giga/util/Utility.java index 793cbea18..dc6a67b4b 100644 --- a/app/src/main/java/us/shandian/giga/util/Utility.java +++ b/app/src/main/java/us/shandian/giga/util/Utility.java @@ -267,8 +267,7 @@ public class Utility { } try { - long length = Long.parseLong(connection.getHeaderField("Content-Length")); - if (length >= 0) return length; + return Long.parseLong(connection.getHeaderField("Content-Length")); } catch (Exception err) { // nothing to do } From cdc8fe86cee3f344f4a54d2fdca1f6f296e62c27 Mon Sep 17 00:00:00 2001 From: kapodamy Date: Mon, 3 Jun 2019 18:39:56 -0300 Subject: [PATCH 136/138] amend rebase resolve inconsistency in string.xml files --- app/src/main/res/values-ca/strings.xml | 3 ++- app/src/main/res/values-cs/strings.xml | 3 ++- app/src/main/res/values-da/strings.xml | 6 +++--- app/src/main/res/values-de/strings.xml | 4 ++-- app/src/main/res/values-el/strings.xml | 3 ++- app/src/main/res/values-es/strings.xml | 23 +++++++++-------------- app/src/main/res/values-et/strings.xml | 3 ++- app/src/main/res/values-fr/strings.xml | 3 ++- app/src/main/res/values-hr/strings.xml | 3 ++- app/src/main/res/values-ja/strings.xml | 4 ++-- app/src/main/res/values-ko/strings.xml | 3 ++- app/src/main/res/values-ru/strings.xml | 1 - app/src/main/res/values-sk/strings.xml | 3 ++- app/src/main/res/values-uk/strings.xml | 3 ++- 14 files changed, 34 insertions(+), 31 deletions(-) diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 04bb36ea3..e631c4fe0 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -446,7 +446,8 @@ Ha fallat la baixada Baixada finalitzada %s baixades finalitzades - Ja existeix un fitxer baixat amb aquest nom + Ja existeix un fitxer baixat amb aquest nom + Ja existeix un fitxer amb aquest nom Hi ha una baixada en curs amb aquest nom Ha fallat la connexió segura No s\'ha pogut trobar el servidor diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 306fb4ccd..97326bfdc 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -459,7 +459,8 @@ otevření ve vyskakovacím okně % s stahování dokončeno Vytvořit jedinečný název Přepsat - Stažený soubor s tímto názvem již existuje + Stažený soubor s tímto názvem již existuje + Stažený soubor s tímto názvem již existuje Stahování s tímto názvem již probíhá Zobrazit chybu Kód diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 0c699cf0e..919cef47b 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -359,7 +359,7 @@ Automatisk Tryk for at downloade Færdig - I kø + Afventning efterbehandling Handling afvist af systemet @@ -368,7 +368,8 @@ %s downloads færdige Generer unikt navn Overskriv - En downloadet fil med dette navn eksisterer allerede + En fil med dette navn eksisterer allerede + En downloadet fil med dette navn eksisterer allerede Der er en download i gang med dette navn Vis fejl Kode @@ -453,5 +454,4 @@ Maksimalt antal forsøg før downloaden opgives Sæt på pause ved skift til mobildata Downloads som ikke kan sættes på pause vil blive genstartet - Afventning \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 9a2518652..7c131eac4 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -448,8 +448,8 @@ %s heruntergeladen Eindeutigen Namen erzeugen Überschreiben - Eine heruntergeladene Datei dieses Namens existiert bereits - Eine Datei dieses Namens wird gerade heruntergeladen + Eine Datei mit diesem Namen existiert bereits + Eine heruntergeladene Datei mit diesem Namen existiert bereits Fehler anzeigen Code Die Datei kann nicht erstellt werden diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 68e93ba58..aa87c9c32 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -444,7 +444,8 @@ %s λήψεις ολοκρηρώθηκαν Δημιουργία μοναδικού ονόματος Αντικατάσταση - Ένα αρχείο με το ίδιο όνομα υπάρχει ήδη + Ένα αρχείο με αυτό το όνομα υπάρχει ήδη + Ένα αρχείο που έχει ληφθεί με αυτό το όνομα υπάρχει ήδη Υπάρχει μια λήψη σε εξέλιξη με αυτό το όνομα Εμφάνιση σφάλματος Κωδικός diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 890755845..8f4375033 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -159,7 +159,7 @@ abrir en modo popup Si tienes ideas de; traducción, cambios de diseño, limpieza de código o cambios de código realmente fuertes—la ayuda siempre es bienvenida. Cuanto más se hace, mejor se pone! Leer licencia Contribuir -Suscribirse + Suscribirse Suscrito Canal no suscrito No se pudo cambiar la suscripción @@ -211,8 +211,8 @@ abrir en modo popup Vídeos Elemento eliminado -¿Desea eliminar este elemento del historial de búsqueda? -Contenido de la página principal + ¿Desea eliminar este elemento del historial de búsqueda? + Contenido de la página principal Página en blanco Página del kiosco Página de suscripción @@ -224,7 +224,7 @@ abrir en modo popup Kiosco Tendencias Top 50 -Mostrar sugerencia cuando se presiona el botón de segundo plano o popup en la página de detalles del vídeo + Mostrar sugerencia cuando se presiona el botón de segundo plano o popup en la página de detalles del vídeo En cola en el reproductor de fondo En cola en el reproductor popup Reproducir todo @@ -242,7 +242,7 @@ abrir en modo popup Comenzar a reproducir aquí Comenzar aquí en segundo plano Comenzar aquí en popup -Mostrar consejo \"Mantener para poner en la cola\" + Mostrar consejo \"Mantener para poner en la cola\" Nuevo y popular Mantener para poner en la cola Donar @@ -270,7 +270,7 @@ abrir en modo popup Reproductor de popup Obteniendo información… Cargando contenido solicitado -Importar base de datos + Importar base de datos Exportar base de datos Reemplazará su historial actual y sus suscripciones Exportar historial, suscripciones y listas de reproducción @@ -404,7 +404,7 @@ abrir en modo popup Listas de reproducción Pistas Finalizadas - En cola + Pendientes pausado en cola post-procesado @@ -423,12 +423,11 @@ abrir en modo popup No se puede sobrescribir el archivo Hay una descarga en curso con este nombre Hay una descarga pendiente con este nombre - Mostrar como grilla Mostrar como lista Limpiar descargas finalizadas - ¿Estas seguro? Tienes %s descargas pendientes, ve a Descargas para continuarlas + ¿Estas seguro? Detener Intentos máximos Cantidad máxima de intentos antes de cancelar la descarga @@ -459,11 +458,7 @@ abrir en modo popup Se perdió el progreso porque el archivo fue eliminado Tiempo de espera excedido - No es posible descargar a una tarjeta SD externa. \¿Restablecer la ubicación de la carpeta de descarga\? - Seleccione los directorios de descarga - Pendiente - Preguntar dónde descargar Se preguntará dónde guardar cada descarga Se preguntará dónde guardar cada descarga.\nHabilita esta opción si quieres descargar en la tarjeta SD externa @@ -480,7 +475,7 @@ abrir en modo popup Notificación de actualización de la aplicación Notificaciones para nueva versión de NewPipe Almacenamiento externo no disponible - Todavía no es posible descargar a una tarjeta SD externa. ¿Restablecer la ubicación de la carpeta de descarga\? + No es posible descargar a una tarjeta SD externa. \¿Restablecer la ubicación de la carpeta de descarga\? Usando las pestañas por defecto, error al leer las pestañas guardadas Restaurar valores por defecto ¿Quieres restaurar los valores por defecto\? diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index a11805f3b..35e2a7695 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -445,7 +445,8 @@ %s allalaadimist lõppenud Loo kordumatu nimi Kirjuta üle - Selle nimega allalaetud fail on juba olemas + Sellise nimega fail on juba olemas + Selle nimega allalaaditud fail on juba olemas Selle nimega allalaadimine on käimas Näita viga Kood diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index ca9682fdb..301d41d36 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -447,7 +447,8 @@ Ajouté à la file d\'attente Générer un nom unique Écraser - Un fichier téléchargé avec ce nom existe déjà + Un fichier avec ce nom existe déjà + Un fichier téléchargé avec ce nom existe déjà Il y a un téléchargement en cours avec ce nom Afficher l\'erreur Code diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index e7aeb924a..3d7601895 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -447,7 +447,8 @@ %s preuzimanja dovršeno Generirajte jedinstveni naziv Piši preko - Preuzeta datoteka s tim nazivom već postoji + Datoteka s tim nazivom već postoji + Preuzeta datoteka s tim nazivom već postoji U tijeku je preuzimanje s ovim nazivom Kod Datoteku nije moguće izraditi diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index be243bffd..ba0e7b1ba 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -432,7 +432,8 @@ %s 件のダウンロード終了 一意の名前を生成します 上書き - 同じ名前のファイルが既に存在します + この名前のファイルは既に存在します + この名前のダウンロードファイルは既に存在します 同じ名前を持つダウンロードが既に進行中です エラーを表示する コード @@ -464,6 +465,5 @@ ダウンロードから %s の保留中の転送を続行します モバイルデータ通信に切り替え時に休止 休止できないダウンロードが再開されます - 保留中 接続タイムアウト \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 1bb71650d..c02b7270e 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -450,7 +450,8 @@ %s 다운로드 완료됨 별개의 이름 생성 덮어쓰기 - 해당 이름을 가진 다운로드된 파일이 이미 존재합니다 + 이 이름을 가진 파일이 이미 있습니다. + 이 이름을 가진 다운로드 된 파일이 이미 있습니다. 해당 이름을 가진 다운로드가 이미 진행중입니다 오류 표시 코드 diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index c75af14e2..b5e3769ad 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -472,6 +472,5 @@ Пост-обработка не удалась Останавливать скачивание при переходе на мобильную сеть Закрыть - в ожидании Время соединения вышло \ No newline at end of file diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index de26a1638..dc53835e4 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -456,7 +456,8 @@ %s sťahovania skončené Vytvoriť jedinečný názov Prepísať - Stiahnutý súbor s týmto menom už existuje + Súbor s týmto názvom už existuje + Stiahnutý súbor s týmto názvom už existuje Sťahovanie s týmto názvom už prebieha Zobraziť chybu Kód diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 60934ebb1..1577371ef 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -443,7 +443,8 @@ %s завантажень завершено Згенерувати унікальну назву Перезаписати - Завантажений файл з таким ім\'ям вже існує + Файл з такою назвою вже існує + Завантажений файл з такою назвою вже існує Файл з такою назвою вже завантажується Показати помилку Код From af971b6a1930bfa59cde473f015826a69c2d44b8 Mon Sep 17 00:00:00 2001 From: Redirion Date: Wed, 12 Jun 2019 14:44:36 +0200 Subject: [PATCH 137/138] Fixed selected subtitle stream not being shown closes #2393 this ammends my obviously incomplete fix in PR #2311. This is just an UI issue. Subtitle track selection works, it just shows "No Captions" unfortunately. --- app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java index 4dbbc571d..d15dc8b28 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -297,6 +297,7 @@ public abstract class VideoPlayer extends BasePlayer return true; }); + // Add all available captions for (int i = 0; i < availableLanguages.size(); i++) { final String captionLanguage = availableLanguages.get(i); MenuItem captionItem = captionPopupMenu.getMenu().add(captionPopupMenuGroupId, @@ -506,7 +507,7 @@ public abstract class VideoPlayer extends BasePlayer } // Normalize mismatching language strings - final String preferredLanguage = trackSelector.getParameters().preferredTextLanguage; + final String preferredLanguage = trackSelector.getPreferredTextLanguage(); // Build UI buildCaptionMenu(availableLanguages); if (trackSelector.getParameters().getRendererDisabled(textRenderer) || From ac5e2e0532abe876bedee8984235a92a29345053 Mon Sep 17 00:00:00 2001 From: kapodamy Date: Fri, 14 Jun 2019 12:19:50 -0300 Subject: [PATCH 138/138] bugs fixes * fix storage warning dialogs created on invalid contexts * implement mkdirs in StoredDirectoryHelper --- .../newpipe/download/DownloadDialog.java | 33 +++++++++++++ .../fragments/detail/VideoDetailFragment.java | 2 +- .../giga/io/StoredDirectoryHelper.java | 46 +++++++++++++++++++ .../giga/service/DownloadManagerService.java | 27 ----------- 4 files changed, 80 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index 8fef9a995..56ea9366d 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -217,6 +217,32 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck okButton.setEnabled(true); context.unbindService(this); + + // check of download paths are defined + if (!askForSavePath) { + String msg = ""; + if (mainStorageVideo == null) msg += getString(R.string.download_path_title); + if (mainStorageAudio == null) + msg += getString(R.string.download_path_audio_title); + + if (!msg.isEmpty()) { + String title; + if (mainStorageVideo == null && mainStorageAudio == null) { + title = getString(R.string.general_error); + msg = getString(R.string.no_available_dir) + ":\n" + msg; + } else { + title = msg; + msg = getString(R.string.no_available_dir); + } + + new AlertDialog.Builder(context) + .setPositiveButton(android.R.string.ok, null) + .setTitle(title) + .setMessage(msg) + .create() + .show(); + } + } } @Override @@ -520,6 +546,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck private void showFailedDialog(@StringRes int msg) { new AlertDialog.Builder(context) + .setTitle(R.string.general_error) .setMessage(msg) .setNegativeButton(android.R.string.ok, null) .create() @@ -631,6 +658,12 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck // This part is called if: // * the filename is not used in a pending/finished download // * the file does not exists, create + + if (!mainStorage.mkdirs()) { + showFailedDialog(R.string.error_path_creation); + return; + } + storage = mainStorage.createFile(filename, mime); if (storage == null || !storage.canWrite()) { showFailedDialog(R.string.error_file_creation); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index bbd1a315d..c89e773f4 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -1195,7 +1195,7 @@ public class VideoDetailFragment downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex); downloadDialog.setSubtitleStreams(currentInfo.getSubtitles()); - downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog"); + downloadDialog.show(getActivity().getSupportFragmentManager(), "downloadDialog"); } catch (Exception e) { ErrorActivity.ErrorInfo info = ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, ServiceList.all() diff --git a/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java b/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java index a65c4dff3..aeb810479 100644 --- a/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java +++ b/app/src/main/java/us/shandian/giga/io/StoredDirectoryHelper.java @@ -144,6 +144,52 @@ public class StoredDirectoryHelper { return docTree == null ? ioTree.exists() : docTree.exists(); } + /** + * Indicates whatever if is possible access using the {@code java.io} API + * + * @return {@code true} for Java I/O API, otherwise, {@code false} for Storage Access Framework + */ + public boolean isDirect() { + return docTree == null; + } + + /** + * Only using Java I/O. Creates the directory named by this abstract pathname, including any + * necessary but nonexistent parent directories. Note that if this + * operation fails it may have succeeded in creating some of the necessary + * parent directories. + * + * @return true if and only if the directory was created, + * along with all necessary parent directories or already exists; false + * otherwise + */ + public boolean mkdirs() { + if (docTree == null) { + return ioTree.exists() || ioTree.mkdirs(); + } + + if (docTree.exists()) return true; + + try { + DocumentFile parent; + String child = docTree.getName(); + + while (true) { + parent = docTree.getParentFile(); + if (parent == null || child == null) break; + if (parent.exists()) return true; + + parent.createDirectory(child); + + child = parent.getName();// for the next iteration + } + } catch (Exception e) { + // no more parent directories or unsupported by the storage provider + } + + return false; + } + public String getTag() { return tag; } diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java index aab0257db..7f3a4bde1 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -270,33 +270,6 @@ public class DownloadManagerService extends Service { Toast.makeText(this, "Permission denied (write)", Toast.LENGTH_SHORT).show(); } - // Check download save paths - - String msg = ""; - if (mManager.mMainStorageVideo == null) - msg += getString(R.string.download_path_title); - else if (mManager.mMainStorageAudio == null) - msg += getString(R.string.download_path_audio_title); - - if (!msg.isEmpty()) { - String title; - if (mManager.mMainStorageVideo == null && mManager.mMainStorageAudio == null) { - title = getString(R.string.general_error); - msg = getString(R.string.no_available_dir) + ":\n" + msg; - } else { - title = msg; - msg = getString(R.string.no_available_dir); - } - - new AlertDialog.Builder(this) - .setPositiveButton(android.R.string.ok, null) - .setTitle(title) - .setMessage(msg) - .create() - .show(); - } - - return mBinder; }