Skip to content

Commit f067bf9

Browse files
ujjol1234david-allison
authored andcommitted
fix(api): cards - list media
Fixed bug which was causing AnkiDroid API to not list media on cards. Due to card rendering being moved to the backend, and Anki using different Regex handling code, we needed to change the implementation of `files_in_str`: [sound:] tags were stripped, for example https://github.com/ankitects/anki/blob/64ca90934bc26ddf7125913abc9dd9de8cb30c2b/pylib/anki/media.py#L136-L150 Fixes 17062
1 parent e7ffe20 commit f067bf9

File tree

4 files changed

+114
-11
lines changed

4 files changed

+114
-11
lines changed

AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/ContentProviderTest.kt

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ package com.ichi2.anki.tests
2121
import android.content.ContentResolver
2222
import android.content.ContentUris
2323
import android.content.ContentValues
24+
import android.database.Cursor
2425
import android.database.CursorWindow
2526
import android.net.Uri
2627
import anki.notetypes.StockNotetype
@@ -49,12 +50,15 @@ import com.ichi2.libanki.getStockNotetype
4950
import com.ichi2.libanki.sched.Scheduler
5051
import com.ichi2.testutils.common.assertThrows
5152
import com.ichi2.utils.emptyStringArray
53+
import kotlinx.serialization.json.Json
5254
import net.ankiweb.rsdroid.exceptions.BackendNotFoundException
5355
import org.hamcrest.MatcherAssert.assertThat
56+
import org.hamcrest.Matchers.allOf
5457
import org.hamcrest.Matchers.containsString
5558
import org.hamcrest.Matchers.equalTo
5659
import org.hamcrest.Matchers.greaterThan
5760
import org.hamcrest.Matchers.greaterThanOrEqualTo
61+
import org.hamcrest.Matchers.hasItem
5862
import org.json.JSONObject.NULL
5963
import org.junit.After
6064
import org.junit.Assert.assertEquals
@@ -67,6 +71,7 @@ import org.junit.Rule
6771
import org.junit.Test
6872
import timber.log.Timber
6973
import kotlin.test.assertNotNull
74+
import kotlin.test.assertTrue
7075
import kotlin.test.junit.JUnitAsserter.assertNotNull
7176

7277
/**
@@ -1345,6 +1350,48 @@ class ContentProviderTest : InstrumentedTest() {
13451350
} ?: fail("query returned null")
13461351
}
13471352

1353+
@Test
1354+
fun testMediaFilesAddedCorrectlyInReviewInfo() {
1355+
val imageFileName = "img.jpg"
1356+
val audioFileName = "test.mp3"
1357+
addNoteUsingBasicNoteType("""Hello <img src="$imageFileName"> [sound:$audioFileName]""")
1358+
.firstCard(col)
1359+
.update {
1360+
queue = QueueType.New
1361+
due = col.sched.today
1362+
}
1363+
1364+
queryReviewInfo { cursor ->
1365+
val media =
1366+
cursor
1367+
.getString(cursor.getColumnIndex(FlashCardsContract.ReviewInfo.MEDIA_FILES))
1368+
.let { Json.decodeFromString<List<String>>(it) }
1369+
1370+
assertThat(
1371+
"media files returned",
1372+
media,
1373+
allOf(
1374+
hasItem(imageFileName),
1375+
hasItem(audioFileName),
1376+
),
1377+
)
1378+
}
1379+
}
1380+
1381+
private fun queryReviewInfo(block: (Cursor) -> Unit) {
1382+
contentResolver
1383+
.query(
1384+
FlashCardsContract.ReviewInfo.CONTENT_URI,
1385+
null,
1386+
null,
1387+
null,
1388+
null,
1389+
)?.use { cursor ->
1390+
assertTrue("has rows") { cursor.moveToFirst() }
1391+
block(cursor)
1392+
}
1393+
}
1394+
13481395
@Test
13491396
fun emptyCards_noCardsInCollection() {
13501397
assertThat("deleted when collection empty", emptyCards(notetypes.cloze), equalTo(0))

AnkiDroid/src/androidTest/java/com/ichi2/anki/tests/InstrumentedTest.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,13 @@ abstract class InstrumentedTest {
151151
}
152152
}
153153

154+
@DuplicatedCode("This should be refactored into a shared library later")
155+
fun Card.update(block: Card.() -> Unit): Card {
156+
block(this)
157+
col.updateCard(this)
158+
return this
159+
}
160+
154161
@DuplicatedCode("This is copied from RobolectricTest. This will be refactored into a shared library later")
155162
protected fun Card.moveToReviewQueue() {
156163
this.queue = QueueType.Rev

AnkiDroid/src/main/java/com/ichi2/anki/provider/CardContentProvider.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1137,7 +1137,7 @@ class CardContentProvider : ContentProvider() {
11371137
FlashCardsContract.ReviewInfo.NEXT_REVIEW_TIMES -> rb.add(nextReviewTimesJson.toString())
11381138
FlashCardsContract.ReviewInfo.MEDIA_FILES ->
11391139
rb.add(
1140-
JSONArray(col.media.filesInStr(currentCard.question(col) + currentCard.answer(col))),
1140+
JSONArray(col.media.filesInStr(currentCard.renderOutput(col))),
11411141
)
11421142
else -> throw UnsupportedOperationException("Queue \"$column\" is unknown")
11431143
}

AnkiDroid/src/main/java/com/ichi2/libanki/Media.kt

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,11 @@ package com.ichi2.libanki
2020
import androidx.annotation.WorkerThread
2121
import anki.media.CheckMediaResponse
2222
import com.google.protobuf.kotlin.toByteString
23+
import com.ichi2.libanki.Media.Companion.htmlMediaRegexps
24+
import com.ichi2.libanki.TemplateManager.TemplateRenderContext.TemplateRenderOutput
2325
import com.ichi2.libanki.exception.EmptyMediaException
2426
import com.ichi2.libanki.utils.LibAnkiAlias
27+
import com.ichi2.libanki.utils.NotInLibAnki
2528
import timber.log.Timber
2629
import java.io.File
2730

@@ -64,18 +67,36 @@ open class Media(
6467
/**
6568
* Extract media filenames from an HTML string.
6669
*
67-
* @param string The string to scan for media filenames ([sound:...] or <img...>).
68-
* @return A list containing all the sound and image filenames found in the input string.
70+
* @param currentCard The card to scan for media filenames ([sound:...] or <img...>).
71+
* @return A distinct, unordered list containing all the sound and image filenames found in the card.
6972
*/
70-
fun filesInStr(string: String): List<String> =
71-
col.backend
72-
.extractAvTags(string, true)
73-
.avTagsList
74-
.filter {
75-
it.hasSoundOrVideo()
76-
}.map {
77-
it.soundOrVideo
73+
@LibAnkiAlias("files_in_str")
74+
fun filesInStr(
75+
renderOutput: TemplateRenderOutput,
76+
includeRemote: Boolean = false,
77+
): List<String> {
78+
val files = mutableListOf<String>()
79+
val processedText = LaTeX.mungeQA(renderOutput.questionText + renderOutput.answerText, col, true)
80+
81+
for (pattern in htmlMediaRegexps) {
82+
val matches = pattern.findAll(processedText)
83+
for (match in matches) {
84+
val fname = pattern.extractFilename(match) ?: continue
85+
val isLocal = !Regex("(?i)https?|ftp://").containsMatchIn(fname)
86+
if (isLocal || includeRemote) {
87+
files.add(fname)
88+
}
7889
}
90+
}
91+
92+
// not in libAnki: the rendered output no longer contains [sound:] tags
93+
files.addAll(
94+
(renderOutput.questionAvTags + renderOutput.answerAvTags)
95+
.filterIsInstance<SoundOrVideoTag>()
96+
.map { it.filename },
97+
)
98+
return files.distinct()
99+
}
79100

80101
fun findUnusedMediaFiles(): List<File> = check().unusedList.map { File(dir, it) }
81102

@@ -123,4 +144,32 @@ open class Media(
123144

124145
@LibAnkiAlias("restore_trash")
125146
fun restoreTrash() = col.backend.restoreTrash()
147+
148+
companion object {
149+
/**
150+
* Given a [media regex][htmlMediaRegexps] and a match, return the captured media filename
151+
*/
152+
@NotInLibAnki
153+
private fun Regex.extractFilename(match: MatchResult): String? {
154+
val index =
155+
when (htmlMediaRegexps.indexOf(this)) {
156+
0, 2 -> 3
157+
1, 3 -> 2
158+
else -> throw IllegalStateException(pattern)
159+
}
160+
return match.groups[index]?.value
161+
}
162+
163+
val htmlMediaRegexps =
164+
listOf(
165+
// src element quoted case (img/audio)
166+
Regex("(?i)(<(?:img|audio)\\b[^>]* src=(['\"])([^>]+?)\\2[^>]*>)"), // Group 3 = fname
167+
// unquoted src (img/audio)
168+
Regex("(?i)(<(?:img|audio)\\b[^>]* src=(?!['\"])([^ >]+)[^>]*?>)"), // Group 2 = fname
169+
// quoted data attribute (object)
170+
Regex("(?i)(<object\\b[^>]* data=(['\"])([^>]+?)\\2[^>]*>)"), // Group 3 = fname
171+
// unquoted data attribute (object)
172+
Regex("(?i)(<object\\b[^>]* data=(?!['\"])([^ >]+)[^>]*?>)"), // Group 2 = fname
173+
)
174+
}
126175
}

0 commit comments

Comments
 (0)