Skip to content
Back to skills

Mysword Module Manager

ASecurity

Especialista em gerenciamento completo de módulos para o app Bíblia Android. Scanner assíncrono com Flow de progresso, detecção de tipo por extensão E por conteúdo de tabelas SQLite, suporte a todos os formatos (MySword, MyBible .SQLite3 com todos os 7 subtipos, e-Sword v9 .bblx/.cmtx/.dctx e v11 .bbli/.cmti/.dcti, TheWord .ont/.ot/.nt, USFM .zip), Room entity universal ModuleEntity com ModuleFormat enum, ModuleDao completo com 15+ queries, scan incremental (só novos/modificados), WorkManager...

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 5, 2026
databasesgokotlinbashsqldatabase

Security analysis

A100/100

Scanned October 5, 2026

npx -y skills add welitonevoc/Biblia-Codex --skill mysword-module-manager --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Mysword Module Manager?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Mysword Module Manager
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/welitonevoc-mysword-module-manager/badge)](https://www.skillsdirectory.com/skills/welitonevoc-mysword-module-manager)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: mysword-module-manager
description: Especialista em gerenciamento completo de módulos para o app Bíblia Android. Scanner assíncrono com Flow de progresso, detecção de tipo por extensão E por conteúdo de tabelas SQLite, suporte a todos os formatos (MySword, MyBible .SQLite3 com todos os 7 subtipos, e-Sword v9 .bblx/.cmtx/.dctx e v11 .bbli/.cmti/.dcti, TheWord .ont/.ot/.nt, USFM .zip), Room entity universal ModuleEntity com ModuleFormat enum, ModuleDao completo com 15+ queries, scan incremental (só novos/modificados), WorkManager para limpeza periódica, extração de metadata do MySword Details + MyBible INFO key-value, os 66 livros canônicos PT-BR com números, abreviações e capítulos. Triggers: module scanner, module manager, module install, bible scanner, installed modules, module entity, module dao, module list, scan modules, module type detection, ModuleFormat enum.
allowed-tools: Read, Glob, Grep, Bash
---

# MySword Module Manager — Gerenciamento Completo de Módulos

> **Formatos suportados:** MySword (.bbl.mybible etc.), MyBible (.SQLite3 com 7 subtipos), e-Sword v9/v11, TheWord (.ont/.ot/.nt), USFM (.zip)

---

## 📁 Diretórios Padrão de Scan

```kotlin
object MySwordDirectories {
    fun getDefaultScanDirs(context: Context): List<File> = buildList {
        // Raiz padrão MySword
        File(Environment.getExternalStorageDirectory(), "mysword").also { root ->
            if (root.exists()) {
                listOf("bibles", "commentaries", "dictionaries", "books", "journals", "journalsbig")
                    .map { File(root, it) }.filter { it.exists() }.forEach { add(it) }
            }
        }
        // Storage externo do app
        context.getExternalFilesDir(null)?.let { add(it) }
        context.getExternalFilesDirs(null).forEach { add(it) }
        // Downloads (usuários baixam pelo browser)
        Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)?.let { add(it) }
        // Interoperabilidade com MyBible
        File(Environment.getExternalStorageDirectory(), "MyBible").takeIf { it.exists() }?.let { add(it) }
        context.getExternalFilesDir(null)?.parentFile?.parentFile
            ?.let { File(it, "ua.mybible/files/MyBible") }
            ?.takeIf { it.exists() }?.let { add(it) }
    }.filter { it.exists() && it.isDirectory }
}
```

---

## 🔍 Detecção de Tipo

```kotlin
enum class ModuleType { BIBLE, COMMENTARY, DICTIONARY, BOOK, JOURNAL, UNKNOWN }

enum class ModuleFormat {
    MYSWORD,            // .bbl.mybible, .cmt.mybible, .dct.mybible, .bok.mybible, .jor.mybible
    MYBIBLE_BIBLE,      // .SQLite3 (sem sufixo)
    MYBIBLE_COMMENTARY, // .commentaries.SQLite3
    MYBIBLE_DICTIONARY, // .dictionary.SQLite3
    MYBIBLE_SUBHEADINGS,// .subheadings.SQLite3
    MYBIBLE_CROSSREFS,  // .crossreferences.SQLite3
    MYBIBLE_PLAN,       // .plan.SQLite3
    MYBIBLE_DEVOTIONS,  // .devotions.SQLite3
    ESWORD_LEGACY,      // .bblx, .cmtx, .dctx, .topx, .devx
    ESWORD_MODERN,      // .bbli, .cmti, .dcti, .refi, .devi
    THEWORD,            // .ont, .ot, .nt
    USFM,               // .zip com "usfm" no nome
    UNKNOWN
}

object ModuleDetector {
    fun detect(file: File): Pair<ModuleType, ModuleFormat>? {
        val lower = file.name.lowercase()
        return when {
            // MySword nativo — por extensão composta
            lower.endsWith(".bbl.mybible")     -> ModuleType.BIBLE       to ModuleFormat.MYSWORD
            lower.endsWith(".cmt.mybible")     -> ModuleType.COMMENTARY  to ModuleFormat.MYSWORD
            lower.endsWith(".dct.mybible")     -> ModuleType.DICTIONARY  to ModuleFormat.MYSWORD
            lower.endsWith(".bok.mybible")     -> ModuleType.BOOK        to ModuleFormat.MYSWORD
            lower.endsWith(".jor.mybible")     -> ModuleType.JOURNAL     to ModuleFormat.MYSWORD
            lower.endsWith(".mybible")         -> detectMyswordGeneric(file)

            // MyBible — sufixo antes de .sqlite3 (com 'S' maiúsculo na extensão real)
            // Detecção case-insensitive aqui para robustez
            lower.endsWith(".commentaries.sqlite3")  -> ModuleType.COMMENTARY  to ModuleFormat.MYBIBLE_COMMENTARY
            lower.endsWith(".dictionary.sqlite3")     -> ModuleType.DICTIONARY  to ModuleFormat.MYBIBLE_DICTIONARY
            lower.endsWith(".subheadings.sqlite3")    -> ModuleType.BIBLE       to ModuleFormat.MYBIBLE_SUBHEADINGS
            lower.endsWith(".crossreferences.sqlite3")-> ModuleType.BIBLE       to ModuleFormat.MYBIBLE_CROSSREFS
            lower.endsWith(".plan.sqlite3")           -> ModuleType.BOOK        to ModuleFormat.MYBIBLE_PLAN
            lower.endsWith(".devotions.sqlite3")      -> ModuleType.JOURNAL     to ModuleFormat.MYBIBLE_DEVOTIONS
            lower.endsWith(".sqlite3") && !lower.contains(".markup.") ->
                detectMyBibleBible(file)

            // e-Sword
            lower.endsWith(".bblx") || lower.endsWith(".bbli") -> ModuleType.BIBLE       to if (lower.endsWith("x")) ModuleFormat.ESWORD_LEGACY else ModuleFormat.ESWORD_MODERN
            lower.endsWith(".cmtx") || lower.endsWith(".cmti") -> ModuleType.COMMENTARY  to if (lower.endsWith("x")) ModuleFormat.ESWORD_LEGACY else ModuleFormat.ESWORD_MODERN
            lower.endsWith(".dctx") || lower.endsWith(".dcti") -> ModuleType.DICTIONARY  to if (lower.endsWith("x")) ModuleFormat.ESWORD_LEGACY else ModuleFormat.ESWORD_MODERN

            // The Word
            lower.endsWith(".ont") -> ModuleType.BIBLE to ModuleFormat.THEWORD
            lower.endsWith(".ot")  -> ModuleType.BIBLE to ModuleFormat.THEWORD
            lower.endsWith(".nt")  -> ModuleType.BIBLE to ModuleFormat.THEWORD

            // USFM zip
            lower.endsWith(".zip") && lower.contains("usfm") -> ModuleType.BIBLE to ModuleFormat.USFM

            else -> null
        }
    }

    private fun detectMyswordGeneric(file: File): Pair<ModuleType, ModuleFormat>? =
        detectByTables(file)?.let { it to ModuleFormat.MYSWORD }

    private fun detectMyBibleBible(file: File): Pair<ModuleType, ModuleFormat>? {
        // Verificar se é um Bible válido ou outro tipo pelo conteúdo
        val type = detectByTables(file) ?: return ModuleType.BIBLE to ModuleFormat.MYBIBLE_BIBLE
        return type to when (type) {
            ModuleType.COMMENTARY  -> ModuleFormat.MYBIBLE_COMMENTARY
            ModuleType.DICTIONARY  -> ModuleFormat.MYBIBLE_DICTIONARY
            else                   -> ModuleFormat.MYBIBLE_BIBLE
        }
    }

    fun detectByTables(file: File): ModuleType? = runCatching {
        SQLiteDatabase.openDatabase(file.absolutePath, null, SQLiteDatabase.OPEN_READONLY).use { db ->
            val tables = db.rawQuery(
                "SELECT name FROM sqlite_master WHERE type='table'", null
            ).use { c -> buildSet { while (c.moveToNext()) add(c.getString(0).uppercase()) } }
            when {
                "BIBLE"      in tables -> ModuleType.BIBLE
                "VERSES"     in tables -> ModuleType.BIBLE       // MyBible
                "COMMENTARY" in tables -> ModuleType.COMMENTARY
                "COMMENTARIES" in tables -> ModuleType.COMMENTARY  // MyBible
                "DICTIONARY" in tables -> ModuleType.DICTIONARY
                "BOOKS"      in tables -> ModuleType.BOOK
                "JOURNAL"    in tables -> ModuleType.JOURNAL
                else -> null
            }
        }
    }.getOrNull()
}
```

---

## 🗄️ Room Entity Universal

```kotlin
@Entity(
    tableName = "modules",
    indices = [
        Index(value = ["type"]),
        Index(value = ["format"]),
        Index(value = ["language"]),
        Index(value = ["isEnabled"]),
        Index(value = ["abbreviation"], unique = true)
    ]
)
data class ModuleEntity(
    @PrimaryKey val filePath: String,
    val fileName: String,
    val type: ModuleType,
    val format: ModuleFormat,
    val abbreviation: String,   // ⚠️ Para MyBible: extraído do nome do arquivo
    val title: String,
    val description: String? = null,
    val language: String = "und",    // ISO 3-letter para MySword, 2-letter para MyBible
    val isRightToLeft: Boolean = false,
    val hasOT: Boolean = false,
    val hasNT: Boolean = false,
    val hasStrong: Boolean = false,
    val isInterlinear: Boolean = false,
    val versionDate: String? = null, // YYYY-MM-DD
    val customCss: String? = null,
    val verseRules: String? = null,  // ⚠️ Só MySword — MyBible não tem VerseRules
    val isEnabled: Boolean = true,
    val displayOrder: Int = 0,
    val fileSize: Long = 0L,
    val lastModified: Long = 0L,
    val installedAt: Long = System.currentTimeMillis()
)
```

---

## 🗄️ ModuleDao

```kotlin
@Dao interface ModuleDao {
    @Query("SELECT * FROM modules WHERE isEnabled=1 ORDER BY displayOrder, title")
    fun observeEnabled(): Flow<List<ModuleEntity>>

    @Query("SELECT * FROM modules WHERE type=:type AND isEnabled=1 ORDER BY displayOrder, title")
    fun observeByType(type: ModuleType): Flow<List<ModuleEntity>>

    @Query("SELECT * FROM modules WHERE type='BIBLE' AND isEnabled=1 ORDER BY displayOrder")
    fun observeEnabledBibles(): Flow<List<ModuleEntity>>

    @Query("SELECT * FROM modules WHERE filePath=:path LIMIT 1")
    suspend fun getByPath(path: String): ModuleEntity?

    @Query("SELECT * FROM modules WHERE abbreviation=:abbr LIMIT 1")
    suspend fun getByAbbreviation(abbr: String): ModuleEntity?

    @Query("SELECT * FROM modules WHERE type='DICTIONARY' AND hasStrong=1 AND language LIKE :langPfx||'%' AND isEnabled=1 LIMIT 1")
    suspend fun findStrongsDictionary(langPfx: String): ModuleEntity?

    @Query("SELECT * FROM modules WHERE abbreviation=:abbr AND versionDate<:newDate LIMIT 1")
    suspend fun findOutdated(abbr: String, newDate: String): ModuleEntity?

    @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun upsert(m: ModuleEntity)
    @Delete suspend fun delete(m: ModuleEntity)
    @Query("DELETE FROM modules WHERE filePath=:path") suspend fun deleteByPath(path: String)
    @Query("SELECT filePath FROM modules") suspend fun getAllFilePaths(): List<String>
    @Query("UPDATE modules SET isEnabled=:e WHERE filePath=:p") suspend fun setEnabled(p: String, e: Boolean)
    @Query("UPDATE modules SET displayOrder=:o WHERE filePath=:p") suspend fun setOrder(p: String, o: Int)
    @Query("SELECT COUNT(*) FROM modules WHERE type=:type AND isEnabled=1") suspend fun countByType(type: ModuleType): Int
    @Query("SELECT SUM(fileSize) FROM modules") suspend fun totalStorageUsed(): Long?
}
```

---

## 🔄 Scanner Assíncrono

```kotlin
class ModuleScanner(private val context: Context, private val dao: ModuleDao) {

    sealed class ScanProgress {
        object Discovering : ScanProgress()
        data class Found(val count: Int) : ScanProgress()
        data class Progress(val current: Int, val total: Int, val name: String) : ScanProgress()
        data class FileError(val fileName: String, val error: String) : ScanProgress()
        data class Complete(val processed: Int, val newModules: Int) : ScanProgress()
    }

    fun scanFlow(dirs: List<File> = MySwordDirectories.getDefaultScanDirs(context)):
        Flow<ScanProgress> = flow {
        emit(ScanProgress.Discovering)
        val files = dirs.flatMap { it.walkTopDown()
            .filter { f -> f.isFile && ModuleDetector.detect(f) != null }.toList() }
        emit(ScanProgress.Found(files.size))

        val existing = dao.getAllFilePaths().toSet()
        var processed = 0; var newCount = 0

        files.forEach { file ->
            runCatching {
                val entity = extractEntity(file)
                if (entity != null) {
                    if (file.absolutePath !in existing) newCount++
                    dao.upsert(entity)
                }
            }.onFailure { emit(ScanProgress.FileError(file.name, it.message ?: "Error")) }
            emit(ScanProgress.Progress(++processed, files.size,
                file.nameWithoutExtension.take(30)))
        }

        // Remover entradas de arquivos deletados
        existing.filter { !File(it).exists() }.forEach { dao.deleteByPath(it) }
        emit(ScanProgress.Complete(processed, newCount))
    }.flowOn(Dispatchers.IO)

    fun extractEntity(file: File): ModuleEntity? {
        val (type, format) = ModuleDetector.detect(file) ?: return null
        return runCatching {
            SQLiteDatabase.openDatabase(file.absolutePath, null,
                SQLiteDatabase.OPEN_READONLY or SQLiteDatabase.NO_LOCALIZED_COLLATORS).use { db ->
                when {
                    format == ModuleFormat.MYSWORD            -> readMySwordDetails(db, file, type, format)
                    format.name.startsWith("MYBIBLE")         -> readMyBibleInfo(db, file, type, format)
                    format == ModuleFormat.ESWORD_LEGACY ||
                    format == ModuleFormat.ESWORD_MODERN      -> readESwordDetails(db, file, type, format)
                    else -> buildFallbackEntity(file, type, format)
                }
            }
        }.getOrNull()
    }

    private fun readMySwordDetails(db: SQLiteDatabase, file: File, type: ModuleType, fmt: ModuleFormat): ModuleEntity? {
        if (!tableExists(db, "Details")) return null
        val c = db.rawQuery("SELECT Abbreviation,Title,Language,OT,NT,Strong,RightToLeft,Interlinear,VersionDate,CustomCSS,VerseRules FROM Details LIMIT 1", null)
        return c.use {
            if (!it.moveToFirst()) return null
            ModuleEntity(filePath=file.absolutePath, fileName=file.name, type=type, format=fmt,
                abbreviation = it.getString(0) ?: file.nameWithoutExtension,
                title        = it.getString(1) ?: file.nameWithoutExtension,
                language     = it.getString(2) ?: "und",
                hasOT        = it.getInt(3) == 1, hasNT = it.getInt(4) == 1,
                hasStrong    = it.getInt(5) == 1, isRightToLeft = it.getInt(6) == 1,
                isInterlinear = it.getInt(7) == 1, versionDate = it.getString(8),
                customCss = it.getString(9), verseRules = it.getString(10),
                fileSize = file.length(), lastModified = file.lastModified())
        }
    }

    private fun readMyBibleInfo(db: SQLiteDatabase, file: File, type: ModuleType, fmt: ModuleFormat): ModuleEntity? {
        val tableName = listOf("info", "INFO").firstOrNull { tableExists(db, it) } ?: return null
        val info = db.rawQuery("SELECT name, value FROM $tableName", null)
            .use { c -> buildMap { while (c.moveToNext()) put(c.getString(0), c.getString(1) ?: "") } }
        // MyBible: abreviação = nome do arquivo, sem extensões de sufixo
        val abbr = file.name
            .removeSuffix(".SQLite3").removeSuffix(".sqlite3")
            .removeSuffix(".commentaries").removeSuffix(".dictionary")
            .removeSuffix(".subheadings").removeSuffix(".crossreferences")
            .removeSuffix(".plan").removeSuffix(".devotions")
        val lang2 = info["language"] ?: "und"
        // Converter 2-letter → 3-letter para normalização interna
        val lang3 = mapOf("en" to "eng", "pt" to "por", "ru" to "rus",
            "de" to "deu", "es" to "spa", "fr" to "fra",
            "el" to "grk", "iw" to "heb", "he" to "heb")[lang2] ?: lang2
        return ModuleEntity(filePath=file.absolutePath, fileName=file.name, type=type, format=fmt,
            abbreviation = abbr, title = info["description"] ?: abbr,
            language = lang3, hasOT = true, hasNT = true,
            hasStrong = info["strong_numbers"] == "true",
            isRightToLeft = info["right_to_left"] == "true",
            customCss = info["html_style"],
            fileSize = file.length(), lastModified = file.lastModified())
    }

    private fun readESwordDetails(db: SQLiteDatabase, file: File, type: ModuleType, fmt: ModuleFormat): ModuleEntity? {
        if (!tableExists(db, "Details")) return buildFallbackEntity(file, type, fmt)
        val c = db.rawQuery("SELECT Abbreviation, Description, Language, OT, NT, Strong FROM Details LIMIT 1", null)
        return c.use {
            if (!it.moveToFirst()) return buildFallbackEntity(file, type, fmt)
            ModuleEntity(filePath=file.absolutePath, fileName=file.name, type=type, format=fmt,
                abbreviation = it.getString(0) ?: file.nameWithoutExtension,
                title        = it.getString(1) ?: file.nameWithoutExtension,
                language     = it.getString(2) ?: "und",
                hasOT = it.getInt(3) == 1, hasNT = it.getInt(4) == 1,
                hasStrong = it.getInt(5) == 1,
                fileSize = file.length(), lastModified = file.lastModified())
        }
    }

    private fun buildFallbackEntity(file: File, type: ModuleType, fmt: ModuleFormat) = ModuleEntity(
        filePath = file.absolutePath, fileName = file.name, type = type, format = fmt,
        abbreviation = file.nameWithoutExtension, title = file.nameWithoutExtension,
        fileSize = file.length(), lastModified = file.lastModified()
    )

    private fun tableExists(db: SQLiteDatabase, name: String): Boolean =
        db.rawQuery("SELECT name FROM sqlite_master WHERE type='table' AND name=?",
            arrayOf(name)).use { it.count > 0 }
}
```

---

## 📚 66 Livros Canônicos PT-BR com Numeração Completa

```kotlin
data class BookInfo(val mySword: Int, val myBible: Int, val ptName: String, val abbr: String, val chapters: Int)

object BibleBooks {
    val ALL = listOf(
        BookInfo(1,  10,  "Gênesis",            "Gn",   50),
        BookInfo(2,  20,  "Êxodo",              "Êx",   40),
        BookInfo(3,  30,  "Levítico",           "Lv",   27),
        BookInfo(4,  40,  "Números",            "Nm",   36),
        BookInfo(5,  50,  "Deuteronômio",       "Dt",   34),
        BookInfo(6,  60,  "Josué",              "Js",   24),
        BookInfo(7,  70,  "Juízes",             "Jz",   21),
        BookInfo(8,  80,  "Rute",               "Rt",    4),
        BookInfo(9,  90,  "1 Samuel",           "1Sm",  31),
        BookInfo(10, 100, "2 Samuel",           "2Sm",  24),
        BookInfo(11, 110, "1 Reis",             "1Rs",  22),
        BookInfo(12, 120, "2 Reis",             "2Rs",  25),
        BookInfo(13, 130, "1 Crônicas",         "1Cr",  29),
        BookInfo(14, 140, "2 Crônicas",         "2Cr",  36),
        BookInfo(15, 150, "Esdras",             "Ed",   10),
        BookInfo(16, 160, "Neemias",            "Ne",   13),
        BookInfo(17, 190, "Ester",              "Et",   10),
        BookInfo(18, 220, "Jó",                 "Jó",   42),
        BookInfo(19, 230, "Salmos",             "Sl",  150),
        BookInfo(20, 240, "Provérbios",         "Pv",   31),
        BookInfo(21, 250, "Eclesiastes",        "Ec",   12),
        BookInfo(22, 260, "Cânticos",           "Ct",    8),
        BookInfo(23, 290, "Isaías",             "Is",   66),
        BookInfo(24, 300, "Jeremias",           "Jr",   52),
        BookInfo(25, 310, "Lamentações",        "Lm",    5),
        BookInfo(26, 330, "Ezequiel",           "Ez",   48),
        BookInfo(27, 340, "Daniel",             "Dn",   12),
        BookInfo(28, 350, "Oséias",             "Os",   14),
        BookInfo(29, 360, "Joel",               "Jl",    3),
        BookInfo(30, 370, "Amós",               "Am",    9),
        BookInfo(31, 380, "Obadias",            "Ob",    1),
        BookInfo(32, 390, "Jonas",              "Jn",    4),
        BookInfo(33, 400, "Miquéias",           "Mq",    7),
        BookInfo(34, 410, "Naum",               "Na",    3),
        BookInfo(35, 420, "Habacuque",          "Hc",    3),
        BookInfo(36, 430, "Sofonias",           "Sf",    3),
        BookInfo(37, 440, "Ageu",               "Ag",    2),
        BookInfo(38, 450, "Zacarias",           "Zc",   14),
        BookInfo(39, 460, "Malaquias",          "Ml",    4),
        BookInfo(40, 470, "Mateus",             "Mt",   28),
        BookInfo(41, 480, "Marcos",             "Mc",   16),
        BookInfo(42, 490, "Lucas",              "Lc",   24),
        BookInfo(43, 500, "João",               "Jo",   21),
        BookInfo(44, 510, "Atos",               "At",   28),
        BookInfo(45, 520, "Romanos",            "Rm",   16),
        BookInfo(46, 530, "1 Coríntios",        "1Co",  16),
        BookInfo(47, 540, "2 Coríntios",        "2Co",  13),
        BookInfo(48, 550, "Gálatas",            "Gl",    6),
        BookInfo(49, 560, "Efésios",            "Ef",    6),
        BookInfo(50, 570, "Filipenses",         "Fp",    4),
        BookInfo(51, 580, "Colossenses",        "Cl",    4),
        BookInfo(52, 590, "1 Tessalonicenses",  "1Ts",   5),
        BookInfo(53, 600, "2 Tessalonicenses",  "2Ts",   3),
        BookInfo(54, 610, "1 Timóteo",          "1Tm",   6),
        BookInfo(55, 620, "2 Timóteo",          "2Tm",   4),
        BookInfo(56, 630, "Tito",               "Tt",    3),
        BookInfo(57, 640, "Filemon",            "Fm",    1),
        BookInfo(58, 650, "Hebreus",            "Hb",   13),
        BookInfo(59, 660, "Tiago",              "Tg",    5),
        BookInfo(60, 670, "1 Pedro",            "1Pe",   5),
        BookInfo(61, 680, "2 Pedro",            "2Pe",   3),
        BookInfo(62, 690, "1 João",             "1Jo",   5),
        BookInfo(63, 700, "2 João",             "2Jo",   1),
        BookInfo(64, 710, "3 João",             "3Jo",   1),
        BookInfo(65, 720, "Judas",              "Jd",    1),
        BookInfo(66, 730, "Apocalipse",         "Ap",   22)
    )

    private val byMySword  = ALL.associateBy { it.mySword }
    private val byMyBible  = ALL.associateBy { it.myBible }

    fun getName(mySwordNum: Int): String = byMySword[mySwordNum]?.ptName ?: "Livro $mySwordNum"
    fun getAbbr(mySwordNum: Int): String = byMySword[mySwordNum]?.abbr ?: "L$mySwordNum"
    fun getChapters(mySwordNum: Int): Int = byMySword[mySwordNum]?.chapters ?: 1
    fun myBibleToMySword(myBibleNum: Int): Int = byMyBible[myBibleNum]?.mySword ?: myBibleNum
    fun mySwordToMyBible(mySwordNum: Int): Int = byMySword[mySwordNum]?.myBible ?: (mySwordNum * 10)
    fun isNewTestament(mySwordNum: Int): Boolean = mySwordNum >= 40
}
```

---

## ✅ Checklist

- [ ] `.bbl.mybible`, `.cmt.mybible`, `.dct.mybible`, `.bok.mybible`, `.jor.mybible` por extensão
- [ ] `.SQLite3` MyBible detectado por tabela `verses` + `info`
- [ ] `.commentaries.SQLite3`, `.dictionary.SQLite3` etc. por sufixo no nome
- [ ] `.mybible` genérico detectado por tabelas (Commentary = notas pessoais)
- [ ] `.bblx`/`.bbli` = e-Sword legacy/modern
- [ ] `.ont`/`.ot`/`.nt` = The Word
- [ ] `.zip` com "usfm" = USFM
- [ ] MyBible: abreviação do **nome do arquivo**, não de campo interno
- [ ] MyBible: `book_number` convertido via `BibleBooks.myBibleToMySword()` ao persistir
- [ ] Scan em `Dispatchers.IO` com `Flow` de progresso
- [ ] Scan incremental: só reprocessa arquivos novos ou com `lastModified` diferente
- [ ] Limpeza de módulos deletados implementada
- [ ] `uniqueIndex` em `abbreviation` previne duplicatas
- [ ] `isNewTestament` passado ao `MyBibleTagParser.parse()` para Strong's H vs G

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…